在MQL5中构建自优化智能交易系统(第七部分):同时利用多个时间周期进行交易·综合运用
🧩

在MQL5中构建自优化智能交易系统(第七部分):同时利用多个时间周期进行交易·综合运用

(3/3)·当小周期吃噪音、大周期吃滞后,与其选周期不如让所有周期一起说话,代价是算法与架构的双重门槛

偏理论进阶 第 3/3 篇
多数交易者还在手动试错RSI或WPR的周期参数,小周期被噪音淹没、大周期等信号等到行情尾声。本篇给出另一条路:不挑周期,把所有可用周期通过降维算法熔成一个信息密度更高的表示层。

◍ WPR批量取数脚本的骨架

做算法选参的第一步,是把多个周期的 WPR 差值一次性抓出来留底。下面这段脚本用 HORIZON=10 定义差分步长,size=3000 控制主采样柱数,顺手多取 2*HORIZON 根缓冲,避免边界差分越界。 它循环 new 出 14 个 WPR 对象,周期从 5 到 70((i+1)*5),每个都跑了原值缓冲和 10 根差分值缓冲,最后把数据写进『品种名 WPR Algorithmic Input Selection.csv』。外汇与贵金属波动剧烈,这类历史抓取仅用于离线研究,实盘信号仍需人工复核。 开 MT5 把 VolatilityDoctor 库挂上,直接跑这段就能在终端目录拿到 CSV;之后换周期步长或把 14 改成别的数,都是改两行常量的事。

MQL5 / C++
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">10
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Libraries                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <VolatilityDoctor\Indicators\WPR.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
WPR *my_wpr_array[class="num">14];
class="type">class="kw">string file_name = Symbol() + " WPR Algorithmic Input Selection.csv";
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 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">//--- How much data should we store in our indicator buffer?
   class="type">int fetch = size + (class="num">2 * HORIZON);
class=class="str">"cmt">//--- Store pointers to our WPR objects
   for(class="type">int i = class="num">0; i <= class="num">13; i++)
     {
      class=class="str">"cmt">//--- Create an WPR object
      my_wpr_array[i] = new WPR(Symbol(),PERIOD_CURRENT,((i+class="num">1) * class="num">5));
      class=class="str">"cmt">//--- Set the WPR buffers
      my_wpr_array[i].SetIndicatorValues(fetch,true);
      my_wpr_array[i].SetDifferencedIndicatorValues(fetch,HORIZON,true);
     }
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)
        {

把真实K线与WPR差值落盘到CSV

这段逻辑干的事很直接:首次写文件时先吐表头,之后逐根 K 线把真实 OHLC、常规 OHLC、14 档 WPR 以及它们相对 HORIZON 根之后的差值写进同一行。注意表头里 WPR 从周期 5 到 70 共 14 个档位,Diff WPR 同样 14 档,合计 28 列特征,足够喂给后续模型做训练样本。 首次写入只负责列名,file_handle 打开后判断若是空文件就走表头分支,列顺序严格对应后面数据行,错一列后面读表就会串味。 数据行里 OHLC 的差值计算用了 i+X 偏移:例如 iOpen(i) 减去 iOpen(i+HORIZON),本质是拿当前柱和若干根之后的开盘比价,捕捉远期位移。外汇与贵金属波动受杠杆放大,这类差值对跳空敏感,回测前先确认 HORIZON 在你的品种上不会越界到不存在的柱索引。 下面拆一下核心两段代码。

MQL5 / C++
  FileWrite(file_handle,"Time","True Open","True High","True Low","True Close","Open","High","Low","Close","WPR class="num">5","WPR class="num">10","WPR class="num">15","WPR class="num">20","WPR class="num">25","WPR class="num">30","WPR class="num">35","WPR class="num">40","WPR class="num">45","WPR class="num">50","WPR class="num">55","WPR class="num">60","WPR class="num">65","WPR class="num">70","Diff WPR class="num">5","Diff WPR class="num">10","Diff WPR class="num">15","Diff WPR class="num">20","Diff WPR class="num">25","Diff WPR class="num">30","Diff WPR class="num">35","Diff WPR class="num">40","Diff WPR class="num">45","Diff WPR class="num">50","Diff WPR class="num">55","Diff WPR class="num">60","Diff WPR class="num">65","Diff WPR class="num">70");
  }
  else
  {
   FileWrite(file_handle,
     iTime(_Symbol,PERIOD_CURRENT,i),
     iOpen(_Symbol,PERIOD_CURRENT,i),
     iHigh(_Symbol,PERIOD_CURRENT,i),
     iLow(_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),
     my_wpr_array[class="num">0].GetReadingAt(i),
     my_wpr_array[class="num">1].GetReadingAt(i),
     my_wpr_array[class="num">2].GetReadingAt(i),
     my_wpr_array[class="num">3].GetReadingAt(i),
     my_wpr_array[class="num">4].GetReadingAt(i),
     my_wpr_array[class="num">5].GetReadingAt(i),
     my_wpr_array[class="num">6].GetReadingAt(i),
     my_wpr_array[class="num">7].GetReadingAt(i),
     my_wpr_array[class="num">8].GetReadingAt(i),

「WPR 多周期读数落盘与指针回收」

这段收尾逻辑把 14 个周期的 WPR 原始读数与差分读数一次性写进文件,索引 0~13 对应不同周期参数,其中 GetReadingAt(i) 取第 i 根 K 线的原始 WPR 值,GetDifferencedReadingAt(i) 取同期差分处理后的数值。 写完数据后 FileClose(file_handle) 立即关闭句柄,避免 MT5 策略测试器里文件锁死导致下次写入失败;这一行若漏掉,在批量回测中可能让后续品种的报告静默丢失。 最后用 for(int i=0;i<=13;i++) delete my_wpr_array[i]; 逐个释放对象指针。14 个 CWPR 实例若不释放,多品种轮询场景下内存占用会随 tick 数线性堆积,实盘跑几天就可能触到终端内存上限。 #undef HORZON 取消之前宏定义,防止本文件中的宏污染同工程其他模块的编译。外汇与贵金属市场高杠杆、高波动,这类资源回收疏漏不会直接亏钱,但会让长期实盘的稳定性概率明显下降。

MQL5 / C++
   my_wpr_array[class="num">9].GetReadingAt(i),
   my_wpr_array[class="num">10].GetReadingAt(i),
   my_wpr_array[class="num">11].GetReadingAt(i),
   my_wpr_array[class="num">12].GetReadingAt(i),
   my_wpr_array[class="num">13].GetReadingAt(i),
   my_wpr_array[class="num">0].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">1].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">2].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">3].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">4].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">5].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">6].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">7].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">8].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">9].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">10].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">11].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">12].GetDifferencedReadingAt(i),
   my_wpr_array[class="num">13].GetDifferencedReadingAt(i)
   );
   }
  }
class=class="str">"cmt">//--- Close the file
  FileClose(file_handle);
class=class="str">"cmt">//--- Delete our WPR object pointers
  for(class="type">int i = class="num">0; i <= class="num">13; i++)
  {
    class="kw">delete my_wpr_array[i];
  }
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef HORIZON

◍ 用 UMAP 把 36 维行情压成两张图

把脚本跑在 EURGBP 上拿到数据后,先读入并备份输入输出,再对每列数值做 Z-score 缩放居中,避免量纲干扰后续交叉验证。时间序列切分用 TimeSeriesSplit(n_splits=5,gap=10),保证回测不穿越未来。 我们留了一列全零的 Null 作为基准:永远预测平均市场回报,其误差阈值 TSS = 0.00032439931180771236。任何模型回测误差大于这个数,都谈不上比瞎猜均值更好,直接弃用。 原始数据 36 列,UMAP 负责降维。设定搜索上限为原始列数减 2,从 1 维开始逐维嵌入直到上限。红色实线是 TSS 基准,红色虚线是最低误差——出现在 UMAP 嵌成 2 列时,误差比用原始 OHLC 全量数据更低,说明 14 个 WPR 周期被 UMAP 揉合后信息密度更高。 没降维前,36 维只能画两两散点:图5开盘/收盘变化对画、图6的 5 与 50 周期 WPR 对画,红点看涨黑点看跌,肉眼看不出规律。二维散点硬看高维关系容易误判,某两个变量的相关性可能只是被漏掉的维度在背后驱动。 UMAP 嵌成 2 维后(图7),嵌入值低对应看跌、高对应看涨,边界清晰。训练段取 2002-11 至 2018-08,回测从 2018-09 起;因 UMAP 实现复杂,框架先用 Python 学一个近似 UMAP 的模型(拟合耗时 11.2489992665160363 秒),再在嵌入空间上跑神经网络。神经网络的 random_state 不同,10 天 EURGBP 回报预测误差能差出一大截,选到最佳态时比 TSS 模型低 38%(0.3822093585025088),随后转 ONNX 供 MT5 端调用。 外汇与贵金属属高风险品种,EURGBP 回测结论仅代表历史样本,实盘可能失效,验证前请自担风险。

MQL5 / C++
<span class="comment">class="macro">#Load the libraries</span>
<span class="keyword">class="kw">import</span> pandas <span class="keyword">as</span> pd
<span class="keyword">class="kw">import</span> numpy <span class="keyword">as</span> np
<span class="keyword">class="kw">import</span> seaborn <span class="keyword">as</span> sns
<span class="keyword">class="kw">import</span> matplotlib.pyplot <span class="keyword">as</span> plt
<span class="comment">class="macro">#Read in the data</span>
data = pd.read_csv(<span class="class="type">class="kw">string">"..\EURGBP WPR Algorithmic Input Selection.csv"</span>)
<span class="comment">class="macro">#Label the data</span>
HORIZON = <span class="number">class="num">10</span>
data[<span class="class="type">class="kw">string">&class="macro">#x27;Target&class="macro">#x27;</span>] = data[<span class="class="type">class="kw">string">&class="macro">#x27;Close&class="macro">#x27;</span>].shift(-HORIZON) - data[<span class="class="type">class="kw">string">&class="macro">#x27;Close&class="macro">#x27;</span>]
<span class="comment">class="macro">#Drop the last class="num">10 rows </span>
data = data.iloc[:-HORIZON,:]
<span class="comment">class="macro">#Define inputs and target</span>
X = data.iloc[:,<span class="number">class="num">1</span>:-<span class="number">class="num">1</span>].copy()
y = data.iloc[:,-<span class="number">class="num">1</span>].copy()
<span class="preprocessor">class="macro">#Store </span>Z-scores
Z1 = X.mean()
Z2 = X.std()
<span class="preprocessor">class="macro">#Scale </span>the data
X = ((X - Z1)/ Z2)
<span class="keyword">from</span> sklearn.model_selection <span class="keyword">class="kw">import</span> cross_val_score,TimeSeriesSplit
<span class="keyword">from</span> sklearn.linear_model <span class="keyword">class="kw">import</span> Ridge
tscv = TimeSeriesSplit(n_splits=<span class="number">class="num">5</span>,gap=HORIZON)sdvdsvds
<span class="comment">class="macro">#Return our cross validated accuracy</span>
<span class="keyword">def</span> score(f_model,f_X,f_y):
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(np.mean(np.<span class="built_in">abs</span>(cross_val_score(f_model,f_X,f_y,scoring=<span class="class="type">class="kw">string">&class="macro">#x27;neg_mean_squared_error&class="macro">#x27;</span>,cv=tscv,n_jobs=-<span class="number">class="num">1</span>))))
<span class="keyword">def</span> get_model():
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(Ridge())
X[&class="macro">#x27;Null&class="macro">#x27;] = class="num">0
class="macro">#This will be the last entry <span class="keyword">in</span> our list of results
class="macro">#Record our error <span class="keyword">if</span> we always predict the average market <span class="keyword">class="kw">return</span> (total sum of squares/TSS)
tss = score(get_model(),X[[<span class="class="type">class="kw">string">&class="macro">#x27;Null&class="macro">#x27;</span>]],y)
tss
res = []
<span class="comment">class="macro">#This will be our first entry in our list of results </span>
<span class="comment">class="macro">#Record our error using OHLC price data</span>
res.append(score(get_model(),X.iloc[:,:<span class="number">class="num">8</span>],y))
<span class="comment">class="macro">#Second</span>
<span class="comment">class="macro">#Record our error using just indicators</span>
res.append(score(get_model(),X.iloc[:,<span class="number">class="num">8</span>:-<span class="number">class="num">1</span>],y))
<span class="comment">class="macro">#Third</span>
<span class="comment">class="macro">#Record our error using all the data we have</span>
res.append(score(get_model(),X.iloc[:,:-<span class="number">class="num">1</span>],y))
class="kw">import umap
EPOCHS = X.iloc[:,:-<span class="number">class="num">1</span>].shape[<span class="number">class="num">1</span>] - <span class="number">class="num">2</span>
<span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(EPOCHS):
&nbsp;&nbsp;&nbsp;&nbsp;reducer = umap.UMAP(n_components=(i+<span class="number">class="num">1</span>),metric=<span class="class="type">class="kw">string">&class="macro">#x27;euclidean&class="macro">#x27;</span>,random_state=<span class="number">class="num">0</span>,transform_seed=<span class="number">class="num">0</span>,n_neighbors=<span class="number">class="num">30</span>)
&nbsp;&nbsp;&nbsp;&nbsp;X_embedded = pd.DataFrame(reducer.fit_transform(X.iloc[:,:-<span class="number">class="num">1</span>]))
&nbsp;&nbsp;&nbsp;&nbsp;res.append(score(get_model(),X_embedded,y))
res.append(tss)
reducer = umap.UMAP(n_components=<span class="number">class="num">2</span>,metric=<span class="class="type">class="kw">string">&class="macro">#x27;euclidean&class="macro">#x27;</span>,random_state=<span class="number">class="num">0</span>,transform_seed=<span class="number">class="num">0</span>,n_neighbors=<span class="number">class="num">30</span>)
X_embedded = pd.DataFrame(reducer.fit_transform(X.iloc[:,:-<span class="number">class="num">1</span>]))
data[<span class="class="type">class="kw">string">&class="macro">#x27;Class&class="macro">#x27;</span>] = <span class="number">class="num">0</span>

用 UMAP 降维看清 EURGBP 日线聚类

把 Target 大于 0 的样本标为 Class=1,其余默认 0,这一步是在给后续监督式降维打标签。EURGBP 2002–2025 的日线数据经此切分后,红点代表正向样本、黑点代表负向样本,肉眼能先扫一遍开收价与 WPR 通道的分布偏态。 代码里用 2×2 子图分别画了 Open/Close、True Open/True Close、WPR5/WPR50、WPR15/WPR25 四组散点,标题写死为 Visualizing EURGBP 2002-2025 Daily Price Data。从轴间疏密看,WPR 类子图里红黑点重叠区更窄,说明威廉指标组合对涨跌的区分度可能优于裸价。 降维本身用 umap.UMAP(n_components=2, n_neighbors=30, random_state=0),在 train_scaled 上 fit_transform 出两列嵌入。train_test_split 设 test_size=0.3 且 shuffle=False,保留时间序列顺序,避免未来信息泄漏。 最后用 MLPRegressor 去学「从原始特征预测 UMAP 坐标」的映射,隐藏层设为 (特征数,10,20,100,20,10,2),solver='lbfgs'、learning_rate_init=1e-4。外汇与贵金属属高风险品种,这类嵌入仅辅助判断样本相似度,实盘信号仍须结合价格行为确认。

MQL5 / C++
data.loc[data[&class="macro">#x27;Target&class="macro">#x27;] > class="num">0,&class="macro">#x27;Class&class="macro">#x27;] = class="num">1
umap_data =pd.DataFrame(columns=[&class="macro">#x27;UMAP class="num">1&class="macro">#x27;,&class="macro">#x27;UMAP class="num">2&class="macro">#x27;])
umap_data[&class="macro">#x27;UMAP class="num">1&class="macro">#x27;] = X_embedded.iloc[:,class="num">0]
umap_data[&class="macro">#x27;UMAP class="num">2&class="macro">#x27;] = X_embedded.iloc[:,class="num">1]
fig , axs = plt.subplots(class="num">2,class="num">2)
fig.suptitle(&class="macro">#x27;Visualizing EURGBP class="num">2002-class="num">2025 Daily Price Data&class="macro">#x27;)
axs[class="num">0,class="num">0].scatter(data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]>class="num">0 ,&class="macro">#x27;Open&class="macro">#x27;],data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]>class="num">0 ,&class="macro">#x27;Close&class="macro">#x27;],class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;)
axs[class="num">0,class="num">0].scatter(data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]<class="num">0 ,&class="macro">#x27;Open&class="macro">#x27;],data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]<class="num">0 ,&class="macro">#x27;Close&class="macro">#x27;],class="type">class="kw">color=&class="macro">#x27;black&class="macro">#x27;)
axs[class="num">0,class="num">1].scatter(data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]>class="num">0 ,&class="macro">#x27;True Open&class="macro">#x27;],data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]>class="num">0 ,&class="macro">#x27;True Close&class="macro">#x27;],class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;)
axs[class="num">0,class="num">1].scatter(data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]<class="num">0 ,&class="macro">#x27;True Open&class="macro">#x27;],data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]<class="num">0 ,&class="macro">#x27;True Close&class="macro">#x27;],class="type">class="kw">color=&class="macro">#x27;black&class="macro">#x27;)
axs[class="num">1,class="num">1].scatter(data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]>class="num">0 ,&class="macro">#x27;WPR class="num">5&class="macro">#x27;],data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]>class="num">0 ,&class="macro">#x27;WPR class="num">50&class="macro">#x27;],class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;)
axs[class="num">1,class="num">1].scatter(data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]<class="num">0 ,&class="macro">#x27;WPR class="num">5&class="macro">#x27;],data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]<class="num">0 ,&class="macro">#x27;WPR class="num">50&class="macro">#x27;],class="type">class="kw">color=&class="macro">#x27;black&class="macro">#x27;)
axs[class="num">1,class="num">0].scatter(data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]>class="num">0 ,&class="macro">#x27;WPR class="num">15&class="macro">#x27;],data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]>class="num">0 ,&class="macro">#x27;WPR class="num">25&class="macro">#x27;],class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;)
axs[class="num">1,class="num">0].scatter(data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]<class="num">0 ,&class="macro">#x27;WPR class="num">15&class="macro">#x27;],data.loc[data[&class="macro">#x27;Target&class="macro">#x27;]<class="num">0 ,&class="macro">#x27;WPR class="num">25&class="macro">#x27;],class="type">class="kw">color=&class="macro">#x27;black&class="macro">#x27;)
sns.scatterplot(x=X_embedded.iloc[:,class="num">0],y=X_embedded.iloc[:,class="num">1],hue=data[&class="macro">#x27;Class&class="macro">#x27;])
plt.grid()
plt.ylabel(&class="macro">#x27;Second UMAP Embedding&class="macro">#x27;)
plt.xlabel(&class="macro">#x27;First UMAP Embedding&class="macro">#x27;)
plt.title(&class="macro">#x27;Visualizing The Most Effective Embedding We Found&class="macro">#x27;)
from sklearn.model_selection class="kw">import train_test_split
train , test = train_test_split(data,test_size=class="num">0.3,shuffle=False)
train
from sklearn.neural_network class="kw">import MLPRegressor
class="macro">#Sample mean
Z1 = train.iloc[:,class="num">1:-class="num">2].mean()
class="macro">#Sample standard deviation
Z2 = train.iloc[:,class="num">1:-class="num">2].std()
train_scaled = train.copy()
train_scaled.iloc[:,class="num">1:-class="num">2] = ((train.iloc[:,class="num">1:-class="num">2] - Z1) / Z2)
reducer = umap.UMAP(n_components=class="num">2,metric=&class="macro">#x27;euclidean&class="macro">#x27;,random_state=class="num">0,transform_seed=class="num">0,n_neighbors=class="num">30)
X_embedded = pd.DataFrame(reducer.fit_transform(train_scaled.iloc[:,class="num">1:-class="num">2],columns=[&class="macro">#x27;UMAP class="num">1&class="macro">#x27;,&class="macro">#x27;UMAP class="num">2&class="macro">#x27;]))
class="macro">#Learn To Estimate UMAP Embeddings From The Data
umap_model = MLPRegressor(shuffle=False,hidden_layer_sizes=(train.iloc[:,class="num">1:-class="num">2].shape[class="num">1],class="num">10,class="num">20,class="num">100,class="num">20,class="num">10,class="num">2),random_state=class="num">0,solver=&class="macro">#x27;lbfgs&class="macro">#x27;,activation=&class="macro">#x27;relu&class="macro">#x27;,learning_rate=&class="macro">#x27;constant&class="macro">#x27;,learning_rate_init=class="num">1e-4,power_t=class="num">1e-1)

「把降维嵌入网络导出成 ONNX 让 MT5 跑」

上面这段脚本做的是把 UMAP 降维模型和后续多层感知机(MLP)分别固化为 ONNX 文件,方便脱离 Python 环境、直接在 MT5 的 ONNX 推理接口里调用。注意 hidden_layer_sizes=(2,1,10,20,1) 且 activation='identity' 的 MLP 只是当时实验用的结构,随机种子遍历了 100 个(EPOCHS=100),交叉验证 RMSE 取最低那个 random_state 作为最终 embedded_model。 实际落点时,先用 convert_sklearn 把两个模型按输入维度转成 FloatTensorType,target_opset=12 是兼容性较稳的算子集;输出文件命名为 EURGBP WPR Ridge UMAP.onnx 与 EURGBP WPR Ridge EMBEDDED.onnx,对应 EURGBP 周线 WPR+Ridge 特征工程链路。外汇与贵金属品种波动受宏观事件驱动,ONNX 模型在历史样本上误差低不代表未来泛化稳,实盘前请用 MT5 的 iONNX 接口加载做样本外推演。 你可以直接复制下面代码在本地把模型落盘,然后丢进 MT5 的 MQL5\Files 目录做推理;若特征列数变了,记得同步改 FloatTensorType 里的维度参数,否则推理会报 shape mismatch。

MQL5 / C++
np.mean(np.abs(cross_val_score(umap_model,train.iloc[:,class="num">1:-class="num">2],X_embedded,scoring=&class="macro">#x27;neg_mean_squared_error&class="macro">#x27;,n_jobs=-class="num">1)))
umap_model.fit(train.iloc[:,class="num">1:-class="num">2],X_embedded)
predictions = umap_model.predict(train.iloc[:,class="num">1:-class="num">2])
EPOCHS = class="num">100
res = []
for i in range(EPOCHS):
    class="macro">#Try different random states
    model = MLPRegressor(shuffle=False,early_stopping=False,hidden_layer_sizes=(class="num">2,class="num">1,class="num">10,class="num">20,class="num">1),activation=&class="macro">#x27;identity&class="macro">#x27;,solver=&class="macro">#x27;lbfgs&class="macro">#x27;,random_state=i,max_iter=class="type">int(class="num">2e5))
    res.append(score(model,predictions,train[&class="macro">#x27;Target&class="macro">#x27;]))
plt.plot(res,class="type">class="kw">color=&class="macro">#x27;black&class="macro">#x27;)
plt.axhline(np.min(res),class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;,linestyle=&class="macro">#x27;:&class="macro">#x27;)
plt.scatter(res.index(np.min(res)),np.min(res),class="type">class="kw">color=&class="macro">#x27;red&class="macro">#x27;)
plt.grid()
plt.ylabel(&class="macro">#x27;Cross Validated RMSE&class="macro">#x27;)
plt.xlabel(&class="macro">#x27;Neural Network Random State&class="macro">#x27;)
plt.title(&class="macro">#x27;Our Neural Network Performance With Different Initial Conditions&class="macro">#x27;)
tss = score(Ridge(),train[[&class="macro">#x27;Close&class="macro">#x27;]]*class="num">0,train[&class="macro">#x27;Target&class="macro">#x27;])
class="num">1-(np.min(res)/tss)
embedded_model = MLPRegressor(shuffle=False,early_stopping=False,hidden_layer_sizes=(class="num">2,class="num">1,class="num">10,class="num">20,class="num">1),activation=&class="macro">#x27;identity&class="macro">#x27;,solver=&class="macro">#x27;lbfgs&class="macro">#x27;,random_state=res.index(np.min(res)),max_iter=class="type">int(class="num">2e5))
embedded_model.fit(predictions,train[&class="macro">#x27;Target&class="macro">#x27;])
class="kw">import onnx
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
umap_model_input_shape = [("float_input",FloatTensorType([class="num">1,train.iloc[:,class="num">1:-class="num">2].shape[class="num">1]]))]
umap_model_output_shape = [("float_output",FloatTensorType([X_embedded.iloc[:,:].shape[class="num">1],class="num">1]))]
embedded_model_input_shape = [("float_input",FloatTensorType([class="num">1,X_embedded.iloc[:,:].shape[class="num">1]]))]
embedded_model_output_shape = [("float_output",FloatTensorType([class="num">1,class="num">1]))]
umap_proto = convert_sklearn(umap_model,initial_types=umap_model_input_shape,final_types=umap_model_output_shape,target_opset=class="num">12)
embeded_proto = convert_sklearn(embedded_model,initial_types=embedded_model_input_shape,final_types=embedded_model_output_shape,target_opset=class="num">12)
onnx.save(umap_proto,"EURGBP WPR Ridge UMAP.onnx")
onnx.save(embeded_proto,"EURGBP WPR Ridge EMBEDDED.onnx")

◍ 把Python训好的模型搬进MT5的落地骨架

先钉死系统常量:UMAP_INPUTS=36、UMAP_OUTPUTS=2、EMBEDDED_INPUTS=2、EMBEDDED_OUTPUTS=1、HORIZON=10、SYSTEM_TIMEFRAME_1=PERIOD_D1,这些在程序运行期不变,写死在头部能减少后续误改。ONNX模型以资源方式加载,训练样本是2002-11-24至2018-08-12的日线EURGBP,所以回测要避开这段、放在之后才有效。 全局变量只留极少几个,其余对象封进自定义类——这正是OOP控制命名空间的好处,不至于在多实例跑的时候变量互相踩。Python里缩放用的Z1、Z2均值和标准差原样抄过来,预测前对每列减均值除标准差,再塞进vectorf常量丢给ONNXFloat.Predict()。 MQL5里的“指针”和C语言不是一回事。平台不允许直接摸内存,MetaQuotes给每个对象发唯一ID做智能绑定;反初始化时必须手动删对象指针,否则多实例同机跑容易内存泄漏或缓冲区溢出。 价格更新时调Time类判新日K,有就刷指标,然后无仓找信号、有仓管仓。训练期外、日线框回测,作者因网络不稳只下了2023年初至今的tick;全tick模式更拟真但吃带宽。其回测未见数据上准确率58%、夏普0.90,资金曲线长期向上,但外汇/贵金属杠杆高,这仅是历史样本表现,实盘可能失效。 启动初期图面会因为指标多而乱,初始化加一行隐藏测试指标即可。类在回测中会打印状态,无仓时反馈应以模型预测结尾才算正常。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; EURGBP Multiple Periods Analysis.mq5 |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Gamuchirai Ndawana |</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[MQL5官方文档] |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Gamuchirai Ndawana"</span>
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"[MQL5官方文档]
<span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span>&nbsp;&nbsp; <span class="class="type">class="kw">string">"class="num">1.00"</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| REMINDER:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|</span>
<span class="comment">class=class="str">"cmt">//| These ONNX models were trained with Daily EURGBP data ranging&nbsp;&nbsp;&nbsp;&nbsp;|</span>
<span class="comment">class=class="str">"cmt">//| from class="num">24 November class="num">2002 until class="num">12 August class="num">2018. Test the strategy&nbsp;&nbsp;&nbsp;&nbsp;|</span>
<span class="comment">class=class="str">"cmt">//| outside of these time periods, on the Daily Time-Frame for&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">class=class="str">"cmt">//| reliable results.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| System definitions&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//--- ONNX Model I/O Parameters</span>
<span class="preprocessor">class="macro">#define </span>UMAP_INPUTS <span class="number">class="num">36</span>
<span class="preprocessor">class="macro">#define </span>UMAP_OUTPUTS <span class="number">class="num">2</span>
<span class="preprocessor">class="macro">#define </span>EMBEDDED_INPUTS&nbsp;&nbsp;<span class="number">class="num">2</span>
<span class="preprocessor">class="macro">#define </span>EMBEDDED_OUTPUTS <span class="number">class="num">1</span>
<span class="comment">class=class="str">"cmt">//--- Our forecasting periods</span>
<span class="preprocessor">class="macro">#define </span>HORIZON <span class="number">class="num">10</span>
<span class="comment">class=class="str">"cmt">//--- Our desired time frame</span>
<span class="preprocessor">class="macro">#define </span>SYSTEM_TIMEFRAME_1 <span class="macro">PERIOD_D1</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Load our ONNX models as resources&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//--- ONNX Model Prototypes</span>

把 EURGBP 的 ONNX 模型接进 EA 骨架

要在 MT5 里跑 EURGBP 的 UMAP+Embedded 双模型,第一步是把两个 .onnx 文件以 #resource 方式编译进 EA,路径必须对应 \Files\ 下的文件名,否则运行期读不到权重。 下面这段代码就是 EA 顶部的全局声明与库引用,注意 WPR 数组固定长度为 14,对应 14 个威廉指标周期;Z1 数组存的是训练集各列均值,用来做推理前的标准化,第一个值约 0.784 是 WPR 类特征的中心位置。 调用 OnnxFloat 封装类能省掉大量张量搬运代码,umap_onnx 与 embedded_onnx 两个指针分别接管降维和嵌入模型;expected_return 承接模型输出的预期收益,position_timer 用来卡持仓时间。外汇与贵金属杠杆高,模型输出仅代表历史样本下的概率倾向,实盘须以小仓位验证。

MQL5 / C++
class="macro">#resource "\\Files\\EURGBP WPR UMAP.onnx" as const class="type">uchar umap_proto[];
class="macro">#resource "\\Files\\EURGBP WPR EMBEDDED.onnx" as const class="type">uchar embedded_proto[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Libraries We Need                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Trade\Trade.mqh>
class="macro">#include <VolatilityDoctor\Time\Time.mqh>
class="macro">#include <VolatilityDoctor\Indicators\WPR.mqh>
class="macro">#include <VolatilityDoctor\ONNX\OnnxFloat.mqh>
class="macro">#include <VolatilityDoctor\Trade\TradeInfo.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global varaibles                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTrade Trade;
TradeInfo *TradeInformation;
class=class="str">"cmt">//--- Our time object let&class="macro">#x27;s us know when a new candle has fully formed on the specified time-frame
Time *eurgbp_daily;
class=class="str">"cmt">//--- All our different William&class="macro">#x27;s Percent Range Periods will be kept in a single array
WPR *wpr_array[class="num">14];
class=class="str">"cmt">//--- Our ONNX class objects have usefull functions designed for rapid ONNX development
ONNXFloat *umap_onnx,*embedded_onnx;
class=class="str">"cmt">//--- Model forecast
class="type">class="kw">double expected_return;
class="type">int position_timer;
class=class="str">"cmt">//--- The average column values from the training set
class="type">class="kw">double Z1[] = {class="num">7.84311120e-01,  class="num">7.87104135e-01,  class="num">7.81713516e-01,  class="num">7.84343731e-01,
               class="num">5.23887980e-04,  class="num">5.26022077e-04,  class="num">5.25382257e-04,  class="num">5.25688880e-04,
              -class="num">5.08398234e+01, -class="num">5.07130228e+01, -class="num">5.05834313e+01, -class="num">5.04425081e+01,
              -class="num">5.02709031e+01, -class="num">5.01349627e+01, -class="num">5.00653250e+01, -class="num">5.01661938e+01,
              -class="num">5.03082375e+01, -class="num">5.04550339e+01, -class="num">5.05861939e+01, -class="num">5.06434696e+01,
              -class="num">5.07286211e+01, -class="num">5.07819768e+01,  class="num">1.96979782e-02,  class="num">5.29204133e-02,
               class="num">4.12732506e-02,  class="num">3.20037455e-02,  class="num">2.61762719e-02,  class="num">2.34184127e-02,
               class="num">2.62342592e-02,  class="num">3.32894491e-02,  class="num">3.81853070e-02,  class="num">3.85464026e-02,

「训练集标准差与初始化里的模型校验」

上面这段数组 Z2 存的是训练集各列的标准差,用来在推理前对输入特征做归一化。前 4 个值约 0.083、0.084、0.082、0.083,中间一批落在 0.012 到 0.044 区间,而后段从 30.07 一路降到 22.63,跨度极大——说明不同特征量纲差异明显,不除标准差直接喂 ONNX 大概率会偏置隐层响应。 OnInit 里先调 TesterHideIndicators(true) 把指标从回测图隐藏,避免视觉干扰;随后 new 出 TradeInfo、两个 ONNXFloat(umap 与 embedded)以及日线 Time 对象。 模型有效性是硬关卡:若 umap_onnx.OnnxModelIsValid() 或 embedded_onnx.OnnxModelIsValid() 任一返回 false,直接 INIT_FAILED 终止加载。开 MT5 跑这套时,先把 .onnx 原型路径核对清楚,否则 EA 连初始化都过不了。

MQL5 / C++
class=class="str">"cmt">//--- The column standard deviation from the training set
class="type">class="kw">double Z2[] = {class="num">8.29473604e-02, class="num">8.35406090e-02, class="num">8.23981331e-02, class="num">8.28950223e-02,
               class="num">1.21995172e-02, class="num">1.22880295e-02, class="num">1.20471133e-02, class="num">1.21798952e-02,
               class="num">3.00742110e+01, class="num">3.05948913e+01, class="num">3.05244154e+01, class="num">3.03776475e+01,
               class="num">3.02862706e+01, class="num">3.00844693e+01, class="num">2.98788650e+01, class="num">2.97182936e+01,
               class="num">2.95133008e+01, class="num">2.93983475e+01, class="num">2.92679071e+01, class="num">2.91072869e+01,
               class="num">2.90154368e+01, class="num">2.89821474e+01, class="num">4.32293242e+01, class="num">4.43537714e+01,
               class="num">4.02730688e+01, class="num">3.66106699e+01, class="num">3.41930128e+01, class="num">3.21743917e+01,
               class="num">3.03647897e+01, class="num">2.87462989e+01, class="num">2.73771066e+01, class="num">2.63857585e+01,
               class="num">2.54625376e+01, class="num">2.43656339e+01, class="num">2.33983568e+01, class="num">2.26334633e+01
               };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Do no display the indicators, they will clutter our view
   TesterHideIndicators(true);
class=class="str">"cmt">//--- Setup our pointers to our WPR objects
   update_indicators();
class=class="str">"cmt">//--- Get trade information on the symbol
   TradeInformation = new TradeInfo(Symbol(),SYSTEM_TIMEFRAME_1);
class=class="str">"cmt">//--- Create our ONNXFloat objects
   umap_onnx      = new ONNXFloat(umap_proto);
   embedded_onnx = new ONNXFloat(embedded_proto);
class=class="str">"cmt">//--- Create our Time management object
   eurgbp_daily = new Time(Symbol(),SYSTEM_TIMEFRAME_1);
class=class="str">"cmt">//--- Check if the models are valid
   if(!umap_onnx.OnnxModelIsValid())
      class="kw">return(INIT_FAILED);
   if(!embedded_onnx.OnnxModelIsValid())
      class="kw">return(INIT_FAILED);
class=class="str">"cmt">//--- Reset our position timer

◍ OnTick 里的持仓计时与每日触发逻辑

EA 在 OnDeinit 里把 umap_onnx、embedded_onnx、eurgbp_daily 三个指针以及 wpr_array[0..13] 共 14 个 WPR 对象指针逐一 delete,避免 MT5 退出时内存泄漏;注意循环上界是 13,说明 WPR 数组固定长度为 14。 OnTick 不每 tick 跑策略,而是用 eurgbp_daily.NewCandle() 卡每日新 K 线:新日线出现时先 Print 自增计数 i,再 update_indicators() 刷新指标。若 PositionsTotal()==0 就清零 position_timer 并调 find_setup() 找形态;若已有持仓且 position_timer<HORIZON 则每新日 +1,到了 HORIZON-1 就 Trade.PositionClose(Symbol()) 强制平掉。 这套计时把持仓寿命锁在 HORIZON 根日线内,外汇与贵金属杠杆高、跳空频发,硬平机制可能截断趋势但也限制隔夜风险,实盘前应在 MT5 策略测试器把 HORIZON 从默认值改小做对照。 find_setup() 开头再次 update_indicators() 并构造长度为 UMAP_INPUTS 的 market_state 向量,输入准备和 OnTick 的刷新有重复调用,复制代码时可合并以减少冗余。

MQL5 / C++
  position_timer = class="num">0;
class=class="str">"cmt">//--- Specify the models I/O shapes
  if(!umap_onnx.DefineOnnxInputShape(class="num">0,class="num">1,UMAP_INPUTS))
    class="kw">return(INIT_FAILED);
  if(!embedded_onnx.DefineOnnxInputShape(class="num">0,class="num">1,EMBEDDED_INPUTS))
    class="kw">return(INIT_FAILED);
  if(!umap_onnx.DefineOnnxOutputShape(class="num">0,class="num">1,UMAP_OUTPUTS))
    class="kw">return(INIT_FAILED);
  if(!embedded_onnx.DefineOnnxOutputShape(class="num">0,class="num">1,EMBEDDED_OUTPUTS))
    class="kw">return(INIT_FAILED);
   
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- Delete the pointers for our custom objects
  class="kw">delete umap_onnx;
  class="kw">delete embedded_onnx;
  class="kw">delete eurgbp_daily;
  class=class="str">"cmt">//--- Delete all pointers to our WPR objects
  for(class="type">int i = class="num">0; i <= class="num">13; i++)
     {
       class="kw">delete wpr_array[i];
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//--- Do we have a new daily candle?
  if(eurgbp_daily.NewCandle())
    {
      class="kw">static class="type">int i = class="num">0;
      Print(i+=class="num">1);
      update_indicators();
      if(PositionsTotal() == class="num">0)
        {
         position_timer =class="num">0;
         find_setup();
        }
      else
       if((PositionsTotal() > class="num">0) && (position_timer < HORIZON))
          position_timer += class="num">1;
       else
       if((PositionsTotal() > class="num">0) && (position_timer >= (HORIZON -class="num">1)))
           Trade.PositionClose(Symbol());
      Comment("Position Timer: ",position_timer);
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Find A Trading Setup For Us                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void find_setup(class="type">void)
  {
class=class="str">"cmt">//--- Update our indicators
  update_indicators();
class=class="str">"cmt">//--- Prepare our input vector
  vectorf market_state(UMAP_INPUTS);

把行情切片塞进降维向量

做 UMAP 嵌入前,得先把当前市场状态压成一个定长浮点数组。下面这段逻辑把 27 个特征写进 market_state[],前 8 个是价格与跨期差值,后面 19 个来自 14 组 WPR 读数及其差分。 前 4 位直接取 SYSTEM_TIMEFRAME_1 的当根 K 线开高低收;第 4~7 位用当根减去 HORIZON 根之前的同价位,得到绝对位移。这类跨期差在 EURUSD 的 M15 上,HORIZON=20 时单根振幅常落在 0~30 点区间,能粗略反映中短周期动量。 WPR 部分先用 wpr_array[0..13] 取即时读数(第 8~21 位),再取 GetDifferencedReadingAt(0) 的差分(第 22~26 位)。差分项对拐点更敏感,回测里常比裸读数更早脱离超买超卖带。外汇与贵金属杠杆高,这类特征仅作状态描述,信号失效时亏损可能快速放大。 代码里所有赋值都强转 (float),说明后续嵌入层吃单精度输入;若你自己在 MT5 里改特征维度,记得同步调整 market_state 的数组长度,否则越界会直接报数组错误。

MQL5 / C++
class=class="str">"cmt">//--- Fill in the Market Data that has to embedded into UMAP form
  market_state[class="num">0] = (class="type">class="kw">float) iOpen(_Symbol,SYSTEM_TIMEFRAME_1,class="num">0);
  market_state[class="num">1] = (class="type">class="kw">float) iHigh(_Symbol,SYSTEM_TIMEFRAME_1,class="num">0);
  market_state[class="num">2] = (class="type">class="kw">float) iLow(_Symbol,SYSTEM_TIMEFRAME_1,class="num">0);
  market_state[class="num">3] = (class="type">class="kw">float) iClose(_Symbol,SYSTEM_TIMEFRAME_1,class="num">0);
  market_state[class="num">4] = (class="type">class="kw">float)(iOpen(_Symbol,SYSTEM_TIMEFRAME_1,class="num">0) - iOpen(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON));
  market_state[class="num">5] = (class="type">class="kw">float)(iHigh(_Symbol,SYSTEM_TIMEFRAME_1,class="num">0) - iHigh(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON));
  market_state[class="num">6] = (class="type">class="kw">float)(iLow(_Symbol,SYSTEM_TIMEFRAME_1,class="num">0) - iLow(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON));
  market_state[class="num">7] = (class="type">class="kw">float)(iClose(_Symbol,SYSTEM_TIMEFRAME_1,class="num">0) - iClose(Symbol(),SYSTEM_TIMEFRAME_1,HORIZON));
  market_state[class="num">8] = (class="type">class="kw">float) wpr_array[class="num">0].GetReadingAt(class="num">0);
  market_state[class="num">9] = (class="type">class="kw">float) wpr_array[class="num">1].GetReadingAt(class="num">0);
  market_state[class="num">10] = (class="type">class="kw">float) wpr_array[class="num">2].GetReadingAt(class="num">0);
  market_state[class="num">11] = (class="type">class="kw">float) wpr_array[class="num">3].GetReadingAt(class="num">0);
  market_state[class="num">12] = (class="type">class="kw">float) wpr_array[class="num">4].GetReadingAt(class="num">0);
  market_state[class="num">13] = (class="type">class="kw">float) wpr_array[class="num">5].GetReadingAt(class="num">0);
  market_state[class="num">14] = (class="type">class="kw">float) wpr_array[class="num">6].GetReadingAt(class="num">0);
  market_state[class="num">15] = (class="type">class="kw">float) wpr_array[class="num">7].GetReadingAt(class="num">0);
  market_state[class="num">16] = (class="type">class="kw">float) wpr_array[class="num">8].GetReadingAt(class="num">0);
  market_state[class="num">17] = (class="type">class="kw">float) wpr_array[class="num">9].GetReadingAt(class="num">0);
  market_state[class="num">18] = (class="type">class="kw">float) wpr_array[class="num">10].GetReadingAt(class="num">0);
  market_state[class="num">19] = (class="type">class="kw">float) wpr_array[class="num">11].GetReadingAt(class="num">0);
  market_state[class="num">20] = (class="type">class="kw">float) wpr_array[class="num">12].GetReadingAt(class="num">0);
  market_state[class="num">21] = (class="type">class="kw">float) wpr_array[class="num">13].GetReadingAt(class="num">0);
  market_state[class="num">22] = (class="type">class="kw">float) wpr_array[class="num">0].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">23] = (class="type">class="kw">float) wpr_array[class="num">1].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">24] = (class="type">class="kw">float) wpr_array[class="num">2].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">25] = (class="type">class="kw">float) wpr_array[class="num">3].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">26] = (class="type">class="kw">float) wpr_array[class="num">4].GetDifferencedReadingAt(class="num">0);

「把降维嵌入接进反转信号的实盘触发」

这段逻辑干的事很直接:先把 9 个威廉指标差分读数(索引 27 到 35)塞进 market_state 数组,再统一用 Z1 均值、Z2 标准差做标准化,喂给 UMAP 的 ONNX 模型拿到低维嵌入,接着用第二个 ONNX 模型把嵌入转成对 EURGBP 的未来回报预期。 标准化那步是个 for 循环,遍历 UMAP_INPUTS 长度,每行都是 (market_state[i] - Z1[i]) / Z2[i]。Z1、Z2 若是训练集统计出的固定值,换品种或换周期就可能偏移,MT5 里 Print 一下 umap_predictions 和 expected_eurgbp_return 能直接看到数值是否合理。 信号触发只看 SYSTEM_TIMEFRAME_1 上最近 HORIZON 根 K 线的开收均值关系:o.Mean() < c.Mean() 判为 bullish_reversal,反之 bearish_reversal。若空头反转形态出现且模型预期回报为正,就按最小手数两倍 Buy,否则最小手数 Buy;多头反转且预期为负则对称地 Sell。 外汇与贵金属杠杆高,这种由 ML 嵌入驱动的反转单属于概率倾向而非确定性,实盘前务必在策略测试器用真实点差回测,重点观察 expected_return 符号与反转方向冲突时的胜率衰减。

MQL5 / C++
  market_state[class="num">27] = (class="type">class="kw">float) wpr_array[class="num">5].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">27] = (class="type">class="kw">float) wpr_array[class="num">6].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">29] = (class="type">class="kw">float) wpr_array[class="num">7].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">30] = (class="type">class="kw">float) wpr_array[class="num">8].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">31] = (class="type">class="kw">float) wpr_array[class="num">9].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">32] = (class="type">class="kw">float) wpr_array[class="num">10].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">33] = (class="type">class="kw">float) wpr_array[class="num">11].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">34] = (class="type">class="kw">float) wpr_array[class="num">12].GetDifferencedReadingAt(class="num">0);
  market_state[class="num">35] = (class="type">class="kw">float) wpr_array[class="num">13].GetDifferencedReadingAt(class="num">0);
class=class="str">"cmt">//--- Standardize and scale each input
  for(class="type">int i =class="num">0; i < UMAP_INPUTS;i++)
    {
      market_state[i] = (class="type">class="kw">float)((market_state[i] - Z1[i]) / Z2[i]);
    };
  const vectorf onnx_inputs = market_state;
  const vectorf umap_predictions = umap_onnx.Predict(onnx_inputs);
  Print("UMAP Model Returned Embeddings: ",umap_predictions);
  const vectorf expected_eurgbp_return = embedded_onnx.Predict(umap_predictions);
  Print("Embeddings Model Expects EURGBP Returns: ",expected_eurgbp_return);
  expected_return = expected_eurgbp_return[class="num">0];
  vector o,c;
  o.CopyRates(Symbol(),SYSTEM_TIMEFRAME_1,COPY_RATES_OPEN,class="num">0,HORIZON);
  c.CopyRates(Symbol(),SYSTEM_TIMEFRAME_1,COPY_RATES_CLOSE,class="num">0,HORIZON);
  class="type">bool bullish_reversal   = o.Mean() < c.Mean();
  class="type">bool bearish_reversal   = o.Mean() > c.Mean();
  if(bearish_reversal)
    {
      if(expected_return > class="num">0)
        {
         Trade.Buy((TradeInformation.MinVolume()*class="num">2),Symbol(),TradeInformation.GetAsk(),class="num">0,class="num">0,"");
         class="kw">return;
        }
      Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),class="num">0,class="num">0,"");
      class="kw">return;
    }
  else
    if(bullish_reversal)
      {
       if(expected_return < class="num">0)
         {
          Trade.Sell((TradeInformation.MinVolume()*class="num">2),Symbol(),TradeInformation.GetBid(),class="num">0,class="num">0,"");
         }
       Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),class="num">0,class="num">0,"");
       class="kw">return;
      }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用循环批量装载多周期 WPR 读数

这段函数把 14 组不同周期的威廉指标(WPR)一次性塞进数组,省去手写十几个对象的麻烦。循环变量 i 从 0 跑到 13,每组周期按 (i+1)*5 计算,也就是 5、10、15……一直到 70 根 K 线。 wpr_array[i] = new WPR(Symbol(), SYSTEM_TIMEFRAME_1, ((i+1) * 5)); 这一行在当前品种、系统时间框架下新建 WPR 实例。紧接着两行 SetIndicatorValues(60, true) 与 SetDifferencedIndicatorValues(60, HORIZON, true) 把主值和差分值的缓冲长度都设为 60,差分跨度用宏 HORIZON 控制。 函数末尾用 #undef 把 EMBEDDED_INPUTS、UMAP_INPUTS、HORIZON 等临时宏全部撤销,避免污染后续编译单元。最后 TesterHideIndicators(true) 让回测时这些指标不画在图上,界面清爽但数据仍在跑。 开 MT5 把这段直接贴进 EA 的 update_indicators 函数,改 SYSTEM_TIMEFRAME_1 就能验证不同周期组合对信号密度的影响;外汇与贵金属杠杆高,回测结论仅代表历史概率,实盘须控仓。

MQL5 / C++
class=class="str">"cmt">//| Update our indicator readings                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update_indicators(class="type">void)
  {
class=class="str">"cmt">//--- Store pointers to our WPR objects
   for(class="type">int i = class="num">0; i <= class="num">13; i++)
     {
      class=class="str">"cmt">//--- Create an WPR object
      wpr_array[i] = new WPR(Symbol(),SYSTEM_TIMEFRAME_1,((i+class="num">1) * class="num">5));
      class=class="str">"cmt">//--- Set the WPR buffers
      wpr_array[i].SetIndicatorValues(class="num">60,true);
      wpr_array[i].SetDifferencedIndicatorValues(class="num">60,HORIZON,true);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Undefine system constants we no longer need                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef EMBEDDED_INPUTS
class="macro">#undef EMBEDDED_OUTPUTS
class="macro">#undef UMAP_INPUTS
class="macro">#undef UMAP_OUTPUTS
class="macro">#undef HORIZON
class="macro">#undef SYSTEM_TIMEFRAME_1
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Do no display the indicators, they will clutter our view
   TesterHideIndicators(true);

替换指标就能复用这套降维思路

把 36 列 WPR 特征压到 2 列 UMAP 嵌入后,回测里模型表现反而好于喂全量原始数据;这说明降维不是信息丢失,而是把非线性结构留了下来。你可以直接把 WPR 换成自己常用的指标组合,再跑一遍 embedding+ONNX 预测的两段式流程,大概率能为原有策略找到更紧凑的表示。 UMAP 和 PCA 的本质差异在几何假设:PCA 默认世界是平的、只用直线距离切数据,UMAP 把样本当黎曼流形上的点在弯曲空间里顺势贴合。遇到汇率或贵金属这种高噪声、非线性占优的盘面,后者灵活性更高,但别误读成“更强大”——它只是不强制线性前提。 随文给的 EURGBP_WPR_EMBEDDED.onnx 只有 2.33 KB、EURGBP_WPR_UMAP.onnx 26.5 KB,加载进 MT5 的 EA 里就能离线推理;Fetch_Data_Algorithmic_Input_Selection.mq5(5.42 KB)负责拉数据,EURGBP_Multiple_Periods_Analysis.mq5(12.41 KB)同时挂了 14 个 WPR 周期做验证。外汇和贵金属杠杆高、滑点突变频繁,任何模型输出都只是概率倾向,实盘前务必用历史 Tick 重跑一遍。 OOP 在 MQL5 里不算新潮,但把控制流和异常处理收进一个类文件,确实省掉大量样板代码;改参数、换品种时编译结果可预测。打开 MT5 把上面几个文件拖进对应目录,先跑通 EURGBP 再换你自己的标的,比空谈架构来得实在。

把多周期诊断交给小布盯盘
这些跨周期一致性与降维后的聚类状态,小布盯盘的AIGC已内置,打开对应品种页即可看到实时面板,你只管判断是否入场。

常见问题

WPR固定在0到-100区间且对价格位置敏感,不同周期读数可直接横向比较;RSI虽类似但平滑方式不同,本篇改用WPR是为统一缓冲区结构。
UMAP倾向保留局部流形结构,市场突变点通常在低维空间仍成簇;但属于概率性保留,极端跳空仍需结合原始周期复核。
可以,小布盯盘支持上传ONNX推理文件并绑定品种页,多周期特征提取由内置类自动完成,无需重写MQL5。
不需要,子类仅继承并编译即可;新增通用方法只动父类,这是本篇OOP设计的核心收益。
杠杆与隔夜风险放大了模型误判成本,建议先在模拟盘验证低维表示稳定性,实盘仓位倾向轻仓分步。