突破机器学习的局限(第一部分):缺乏可互操作的度量指标·综合运用
📘

突破机器学习的局限(第一部分):缺乏可互操作的度量指标·综合运用

第 3/3 篇

用双模型对冲检验市场理解力

前文已说明,RMSE 这类指标容易被“永远预测平均收益”的空模型刷到最优,却对实战毫无意义。我们需要一种能验证模型是否真懂市场、能区分盈亏、且不被均值依赖带偏的流程。 这里给出一种叫动态状态切换(DRS)的架构思路:建两个结构完全相同、但假设对立的模型——一个只认趋势跟踪,一个只认均值回归。两者独立训练、互不通信,分别模拟同一策略的多空对立版本。 按有效市场假说,同资产等量买卖并同步开关仓(无手续费)总盈利为 0。若两个模型真学到了底层结构,它们在任何时刻的预测总和应一直为 0;这不是靠指标协调出来的,而是单独训练后的自然结果。我们在训练集拟合后,拿到样本外压测,看预测和是否仍恒为 0——这比盯 RMSE 更能说明问题。 外汇与贵金属属高风险品种,模型验证不等于实盘保本。DRS 中任一时刻只可能一个模型获正回报,预测和偏离 0 往往意味着模型偷学了方向偏差;若和恒为 0,则可更有把握在两种隐藏市态间动态切模型。

「把指标和价差一起落盘」

做价格行为建模时,单拿收盘价不够用。想让后续训练集有信息量,得把技术指标的当前值和它们相对于前一根的变动量一并记录下来,MT5 脚本能直接帮你批量导出。 下面这段脚本定义了 5 周期 SMA 和 10 根的前瞻窗口,默认抓取 3000 根 K 线,并额外多取 HORIZON*2 根避免边界缺失。最终写出以「品种名 DRS Modelling.csv」命名的文件,表头含开高低收、各自 Delta 及 SMA 与其 Delta。 代码里 size 是可调输入,外汇与贵金属波动大、跳空频繁,建议先在 EURUSD 的 M15 上跑一小批验证列对齐,再放大到 3000;此类品种杠杆高,实盘前务必用历史数据回测确认字段含义。

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">//--- Define our moving average indicator
class="macro">#define MA_PERIOD class="num">5            class=class="str">"cmt">//--- Moving Average Period
class="macro">#define MA_TYPE   MODE_SMA     class=class="str">"cmt">//--- Type of moving average we have
class="macro">#define HORIZON class="num">10
class=class="str">"cmt">//--- Our handlers for our indicators
class="type">int ma_handle;
class=class="str">"cmt">//--- Data structures to store the readings from our indicators
class="type">class="kw">double ma_reading[];
class=class="str">"cmt">//--- File name
class="type">class="kw">string file_name = Symbol() + " DRS Modelling.csv";
class=class="str">"cmt">//--- Amount of data requested
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="type">int fetch = size + (HORIZON * class="num">2);
class=class="str">"cmt">//---Setup our technical indicators
   ma_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,class="num">0,MA_TYPE,PRICE_CLOSE);
class=class="str">"cmt">//---Set the values as series
   CopyBuffer(ma_handle,class="num">0,class="num">0,fetch,ma_reading);
   ArraySetAsSeries(ma_reading,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)
        {
         FileWrite(file_handle,"Time","Open","High","Low","Close","Delta O","Delta H","Delta Low","Delta Close","SMA class="num">5","Delta SMA class="num">5");
        }
      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i),

◍ 把K线与均线差值一次性灌进数据集

这段代码片段干的事很直接:用 iOpen / iHigh / iLow / iClose 把当前品种、当前周期下第 i 根 K线的四个价格和 HORIZON 根之后的对应价格做差,连同 ma_reading 的当期与远期差值,一起写进外部文件。 注意所有取价函数都用了 _Symbol 和 PERIOD_CURRENT,意味着你切到黄金 XAUUSD 的 M15,它就按这个上下文抓数;HORIZON 是外部定义的偏移量,决定「未来相对当前」的跨度,回测前务必确认它不为 0 否则差值全塌。 最后 FileClose(file_handle) 收尾,若 file_handle 打开失败前面写入会静默丢数据,实盘前先在 MT5 策略测试器跑一次看文件行数是否等于样本柱数。外汇与贵金属杠杆高,这类特征工程只是给模型喂料,信号失效概率不低。

MQL5 / C++
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)),
 ma_reading[i],
 ma_reading[i] - ma_reading[(i + HORIZON)]
 ); 
   } 
  } 
class=class="str">"cmt">//--- Close the file 
  FileClose(file_handle); 
 } 
class=class="str">"cmt">//+------------------------------------------------------------------+

双状态标注与独立模型的压力测试

先把标准库导进来,读回之前从 MT5 导出的 EURUSD 的 CSV。注意预测步长必须和写 MQL5 脚本时保持一致——当时设的是未来 10 个步长(HORIZON=10),这里若改了,标签和模型就全错位了。 数据标注分两类:一类永远假设市场走趋势,价格上穿 SMA5 就买、下穿就卖;另一类永远假设均值回归,信号正好反过来。把两条累计盈亏曲线叠在一起看,样本段内欧元兑美元均值回归的盈利时间明显多于趋势跟踪,但两条线都有突发跳变,这些异常大概率对应市场状态突然切换。 模型拆成两个完全独立的视角(Dual-View),各自学各自的目标变量,彼此不共享任何梯度或参数。验证集对它们是纯未见样本,相当于拿陌生数据做极端环境压力测试,看是否还理性。 测试判据来自有效市场假说:两个对立策略同时跑,收益应相互抵消。所以两模型预测值相加总和的最大值为 0.0 才通过。实际跑的时候每次只取单模型信号,靠自动切换机制决定用哪个,不人工干预。本次测试通过:True,NumPy 的 norm 检查确认 33 个预测元素全为 0.0。 把趋势模型预测值对上真实盈利曲线,除第 600–700 天那段 EURUSD 剧烈波动没抓住,其余常规幅度都跟住了。模型可导出 ONNX——它是原模型的序列化文件,跨语言都能调,先装 skl2onnx 和 onnx 再定义输入输出形状即可保存。外汇与贵金属属高风险品种,状态切换误判可能直接放大回撤,上实盘前务必用 MT5 重跑这段标注逻辑。

MQL5 / C++
class="macro">#Load our libraries
class="kw">import pandas as pd
class="kw">import numpy as np
class="kw">import matplotlib.pyplot as plt
class="kw">import seaborn as sns
class="macro">#Read in the data
data = pd.read_csv("/content/drive/MyDrive/Colab Data/Financial Data/FX/EUR USD/DRS Modelling/EURUSD DRS Modelling.csv")
data
class="macro">#Recall that in our MQL5 Script our forecast horizon was class="num">10
HORIZON = class="num">10
class="macro">#Calculate the returns generated by the market
data[&class="macro">#x27;Return&class="macro">#x27;] = data[&class="macro">#x27;Close&class="macro">#x27;].shift(-HORIZON) - data[&class="macro">#x27;Close&class="macro">#x27;]
class="macro">#Drop the last horizon rows
data = data.iloc[:-HORIZON,:]
class="macro">#Now let us define the signals being generated by the moving average, in the DRS framework there are always at least n signals depending on the n states the market could be in
class="macro">#Our simple DRS model assumes only class="num">2 states
class="macro">#First we will define the actions you should take assuming the market is in a trending state
class="macro">#Therefore if price crosses above the moving average, buy. Otherwise, sell.
data[&class="macro">#x27;Trend Action&class="macro">#x27;] = class="num">0
data.loc[data[&class="macro">#x27;Close&class="macro">#x27;] > data[&class="macro">#x27;SMA class="num">5&class="macro">#x27;], &class="macro">#x27;Trend Action&class="macro">#x27;] = class="num">1
data.loc[data[&class="macro">#x27;Close&class="macro">#x27;] < data[&class="macro">#x27;SMA class="num">5&class="macro">#x27;], &class="macro">#x27;Trend Action&class="macro">#x27;] = -class="num">1
class="macro">#Now calculate the returns generated by the strategy
data[&class="macro">#x27;Trend Profit&class="macro">#x27;] = data[&class="macro">#x27;Trend Action&class="macro">#x27;] * data[&class="macro">#x27;Return&class="macro">#x27;]
class="macro">#Now we will repeat the procedure assuming the market was mean reverting
data[&class="macro">#x27;Mean Reverting Action&class="macro">#x27;] = class="num">0
data.loc[data[&class="macro">#x27;Close&class="macro">#x27;] > data[&class="macro">#x27;SMA class="num">5&class="macro">#x27;], &class="macro">#x27;Mean Reverting Action&class="macro">#x27;] = -class="num">1
data.loc[data[&class="macro">#x27;Close&class="macro">#x27;] < data[&class="macro">#x27;SMA class="num">5&class="macro">#x27;], &class="macro">#x27;Mean Reverting Action&class="macro">#x27;] = class="num">1
class="macro">#Now calculate the returns generated by the strategy
data[&class="macro">#x27;Mean Reverting Profit&class="macro">#x27;] = data[&class="macro">#x27;Mean Reverting Action&class="macro">#x27;] * data[&class="macro">#x27;Return&class="macro">#x27;]
class="macro">#If we plot our cumulative profit sums, we can see the profit and losses aren&class="macro">#x27;t evenly distributed between the two states
plt.plot(data[&class="macro">#x27;Trend Profit&class="macro">#x27;].cumsum(),class="type">color=&class="macro">#x27;black&class="macro">#x27;)
plt.plot(data[&class="macro">#x27;Mean Reverting Profit&class="macro">#x27;].cumsum(),class="type">color=&class="macro">#x27;red&class="macro">#x27;)
class="macro">#The mean reverting strategy appears to have been making outsized profits with respect to the trending stratetefgy
class="macro">#However, closer inspection reveals, that both strategies are profitable, but never at the same time!
class="macro">#The profit profiles of both strategies show abrupt shocks, when the opposite strategy become more profitable.
plt.legend([&class="macro">#x27;Trend Profit&class="macro">#x27;,&class="macro">#x27;Mean Reverting Profit&class="macro">#x27;])
plt.xlabel(&class="macro">#x27;Time&class="macro">#x27;)
plt.ylabel(&class="macro">#x27;Profit/Loss&class="macro">#x27;)
plt.title(&class="macro">#x27;A DRS Model Visualizes The Market As Being in class="num">2 Possible States&class="macro">#x27;)
plt.grid()
plt.axhline(class="num">0,class="type">color=&class="macro">#x27;black&class="macro">#x27;,linestyle=&class="macro">#x27;:&class="macro">#x27;)
class="macro">#Let&class="macro">#x27;s define the inputs and target
X = data.iloc[:,class="num">1:-class="num">5].columns
y = [&class="macro">#x27;Trend Profit&class="macro">#x27;,&class="macro">#x27;Mean Reverting Profit&class="macro">#x27;]

「把双模型预测导出成 ONNX 给 MT5 用」

上面这段 Python 把趋势模型和均值回归模型分别拟合在 f_train 上,趋势盯 y[0]、均值盯 y[1],验证集预测相加后得到 test_result。代码里用 np.linalg.norm(test_result, ord=2) == 0.0 做零误差断言,实际样本外几乎不可能严格为 0,只是用来快速看管道是否通。 可视化部分画的是 EURUSD 10 日收益的样本外实际利润(黑线)与趋势模型预测利润(红色点线),横轴是 Out Of Sample Days,能直观比对漂移程度。外汇与贵金属杠杆高,样本外误差放大时实盘亏损概率显著上升,别直接当信号源。 真正落地的关键在末尾:用 skl2onnx 把两个 RandomForestRegressor 转成 ONNX,initial_types 设成 [1, len(X)] 的浮点张量,target_opset=12,存成 "EURUSD RF D1 T LBFGSB DRS.onnx" 和 "EURUSD RF D1 M LBFGSB DRS.onnx"。这两个文件可直接丢进 MT5 的 ONNX 推理接口,绕开 Python 常驻进程。 打开 MT5 终端的 ONNX 模块加载这两个文件,输入特征维度必须和训练时 X 的长度一致,否则推理会抛形状错误。先拿历史数据回测 D1 周期,确认两模型加权输出和价格变动方向吻合再考虑接实盘。

MQL5 / C++
from sklearn.model_selection class="kw">import train_test_split,TimeSeriesSplit
from sklearn.ensemble class="kw">import RandomForestRegressor
class="macro">#Split the data
train , test = train_test_split(data,test_size=class="num">0.5,shuffle=False)
f_train , f_validation = train_test_split(train,test_size=class="num">0.5,shuffle=False)
class="macro">#The trend model
trend_model = RandomForestRegressor()
class="macro">#The mean reverting model
mean_model = RandomForestRegressor()
trend_model.fit(f_train.loc[:,X],f_train.loc[:,y[class="num">0]])
mean_model.fit(f_train.loc[:,X],f_train.loc[:,y[class="num">1]])
pred_1 = trend_model.predict(f_validation.loc[:,X])
pred_2 = mean_model.predict(f_validation.loc[:,X])
test_result = pred_1 + pred_2
print(f" Test Passed: {np.linalg.norm(test_result,ord=class="num">2) == class="num">0.0}")
plt.plot(f_validation.loc[:,y[class="num">0]],class="type">color=&class="macro">#x27;black&class="macro">#x27;)
plt.plot(pred_1,class="type">color=&class="macro">#x27;red&class="macro">#x27;,linestyle=&class="macro">#x27;:&class="macro">#x27;)
plt.legend([&class="macro">#x27;Actual Profit&class="macro">#x27;,&class="macro">#x27;Predicted Profit&class="macro">#x27;])
plt.grid()
plt.ylabel(&class="macro">#x27;Loss / Profit&class="macro">#x27;)
plt.xlabel(&class="macro">#x27;Out Of Sample Days&class="macro">#x27;)
plt.title(&class="macro">#x27;Visualizing Our Model Performance Out Of Sample on the EURUSD class="num">10 Day Return&class="macro">#x27;)
!pip install skl2onnx onnx
class="kw">import onnx
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
eurusd_drs_shape = [("float_input",FloatTensorType([class="num">1,len(X)]))]
eurusd_drs_output_shape = [("float_output",FloatTensorType([class="num">1,class="num">1]))]
trend_drs_model_proto = convert_sklearn(trend_model,initial_types=eurusd_drs_shape,final_types=eurusd_drs_output_shape,target_opset=class="num">12)
mean_drs_model_proto = convert_sklearn(mean_model,initial_types=eurusd_drs_shape,final_types=eurusd_drs_output_shape,target_opset=class="num">12)
onnx.save(trend_drs_model_proto,"EURUSD RF D1 T LBFGSB DRS.onnx")
onnx.save(mean_drs_model_proto,"EURUSD RF D1 M LBFGSB DRS.onnx")

◍ 搭系统骨架先定常量与依赖

写 EA 前先把交易系统的常量钉死,后面加载指标和 ONNX 模型才不会乱。下面这段代码定义了回测视野 HORIZON=10(天)、均线周期 MA_PERIOD=5、最小交易量取 SymbolInfoDouble 的 SYMBOL_VOLUME_MIN,相当于给系统装了刻度尺。 系统资源用 #resource 把两个 ONNX 模型塞进字节数组:一个管趋势(EURUSD RF D1 T DRS),一个管均值回归(EURUSD RF D1 M DRS),路径在 \Files\DRS\ 下。CTrade 交易库直接 include 进来,负责下单与持仓管理。 技术指标句柄分开挂:ma_o_handle 和 ma_c_handle 对应开盘/收盘价的 SMA,atr_handle 接 ATR;全局数组 ma_o[]、ma_c[]、atr[] 及 bid/ask、holding_period 用来在 OnTick 里实时刷新。初始化时调一次加载方法,反初始化时手动 ReleaseIndicator 和释放模型,避免 MT5 内存泄漏。 回测务必选模型训练集之外的日期,图8 的示意就是这层意思。用『真实 tick 逐 tick』+『随机延迟』模式压测 DRS 架构最贴近实盘。首次拿 DRS 替代 RMSE 做目标函数就跑出盈利策略,但资金曲线(图11)显示盈利期与亏损期交替——模型没预判波动率,不过策略有自我修正、倾向回到预期收益轨道。外汇和贵金属杠杆高,这套机制只是概率优势,实盘前请在策略测试器里自己跑一遍验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 EURUSD DRS.mq5 |
class=class="str">"cmt">//|                                         Copyright class="num">2024, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                 [MQL5官方文档] |
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=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| System constants                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define HORIZON class="num">10
class="macro">#define MA_PERIOD class="num">5
class="macro">#define MA_SHIFT class="num">0
class="macro">#define MA_MODE MODE_SMA
class="macro">#define TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| System dependencies                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\Files\DRS\EURUSD RF D1 T DRS.onnx" as class="type">uchar onnx_proto[]   class=class="str">"cmt">//Our Trend Model
class="macro">#resource "\Files\DRS\EURUSD RF D1 M DRS.onnx" as class="type">uchar onnx_proto_2[] class=class="str">"cmt">//Our Mean Reverting Mode
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| System libraries                                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Trade/Trade.mqh>
CTrade Trade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Technical Indicators                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int    ma_o_handle,ma_c_handle,atr_handle;
class="type">class="kw">double ma_o[],ma_c[],atr[];
class="type">class="kw">double bid,ask;
class="type">int    holding_period;
class=class="str">"cmt">//+------------------------------------------------------------------+

EA 骨架与日线触发节奏

这段 MT5 专家顾问的骨架把模型加载和交易触发拆得很清楚:两个 ONNX 句柄 onnx_model 与 onnx_model_2 在 OnInit 里通过 OnnxCreateFromBuffer 从内存缓冲区构建,失败就返回 INIT_FAILED,成功才进入主循环。 OnTick 里用 static datetime time_stamp 锁住日线开盘时间 iTime(Symbol(),PERIOD_D1,0),只有日线切换时才跑 update_variables,避免每 tick 重算。当前无持仓调 find_setup,有持仓调 manage_setup,逻辑分支直接挂在 PositionsTotal() 上。 setup 函数顺手建了 ATR(14) 和两组 MA 句柄(收盘/开盘价各一),MA_PERIOD 等参数来自宏定义。外汇与贵金属波动剧烈,这种日线级触发能降频但也可能漏掉日内拐点,实盘前请在 MT5 策略测试器用 2020—2023 年 XAUUSD 数据验证加载耗时与信号延迟。

MQL5 / C++
class=class="str">"cmt">//| Global variables                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long onnx_model,onnx_model_2;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   if(!setup()) 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">//---
   release();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   class="kw">static class="type">class="kw">datetime time_stamp;
   class="type">class="kw">datetime current_time = iTime(Symbol(),PERIOD_D1,class="num">0);
   if(time_stamp != current_time)
     {
      time_stamp = current_time;
      update_variables();
      if(PositionsTotal() == class="num">0)
        {
         find_setup();
        }
      else
        if(PositionsTotal() > class="num">0)
          {
            manage_setup();
          }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Attempt To Setup Our System Variables                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool setup(class="type">void)
  {
   atr_handle = iATR(Symbol(),PERIOD_CURRENT,class="num">14);
   ma_c_handle = iMA(Symbol(),PERIOD_CURRENT,MA_PERIOD,MA_SHIFT,MA_MODE,PRICE_CLOSE);
   ma_o_handle = iMA(Symbol(),PERIOD_CURRENT,MA_PERIOD,MA_SHIFT,MA_MODE,PRICE_OPEN);
   holding_period = class="num">0;
   onnx_model = OnnxCreateFromBuffer(onnx_proto,ONNX_DEFAULT);
   onnx_model_2 = OnnxCreateFromBuffer(onnx_proto_2,ONNX_DEFAULT);
   if(onnx_model == INVALID_HANDLE)
     {
      Comment("Failed to load Trend DRS model");
      class="kw">return(false);
     }

「双模型句柄与张量形状的初始化校验」

在 MT5 里跑双 ONNX 模型(趋势 DRS + 均值回归 DRS),第一步不是算信号,而是把句柄和张量形状钉死。若第二个模型句柄拿到 INVALID_HANDLE,直接 Comment 报错并 return(false),避免后面空跑。 两个模型的输入形状都设为 {1,10},输出形状设为 {1,1};用 OnnxSetInputShape / OnnxSetOutputShape 逐个绑定,任何一次失败都在图表上写清是哪根模型断了。这套写法能保证后续推理时不会因 shape 不匹配在实时 tick 中抛错。 资源释放要成对写:IndicatorRelease 清掉 ma_c、ma_o 两个均线句柄,OnnxRelease 清掉两个模型。漏掉任意一个,EA 重载几次就可能把终端内存吃满。 update_variables 里用 CopyBuffer 拉数据,ma_c 和 ma_o 各取 HORIZON*2 根(从 shift=1 开始),atr 只取最新 1 根;随后 ArraySetAsSeries 置为时间倒序,保证 ma_c[0] 是最近一根均值。外汇与贵金属杠杆高,这类周期参数错配会直接放大滑点风险。

MQL5 / C++
  if(onnx_model_2 == INVALID_HANDLE)
    {
      Comment("Failed to load Mean Reverting DRS model");
      class="kw">return(false);
    }
  class="type">ulong input_shape[] = {class="num">1,class="num">10};
  class="type">ulong output_shape[] = {class="num">1,class="num">1};
  if(!OnnxSetInputShape(onnx_model,class="num">0,input_shape))
    {
      Comment("Failed to set Trend DRS Model input shape");
      class="kw">return(false);
    }
  if(!OnnxSetInputShape(onnx_model_2,class="num">0,input_shape))
    {
      Comment("Failed to set Mean Reverting DRS Model input shape");
      class="kw">return(false);
    }
  if(!OnnxSetOutputShape(onnx_model,class="num">0,output_shape))
    {
      Comment("Failed to set Trend DRS Model output shape");
      class="kw">return(false);
    }
  if(!OnnxSetOutputShape(onnx_model_2,class="num">0,output_shape))
    {
      Comment("Failed to set Mean Reverting DRS Model output shape");
      class="kw">return(false);
    }
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Free up system resources                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void release(class="type">void)
  {
   IndicatorRelease(ma_c_handle);
   IndicatorRelease(ma_o_handle);
   OnnxRelease(onnx_model);
   OnnxRelease(onnx_model_2);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update our system variables                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update_variables(class="type">void)
  {
   bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
   ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
   CopyBuffer(ma_c_handle,class="num">0,class="num">1,(HORIZON*class="num">2),ma_c);
   CopyBuffer(ma_o_handle,class="num">0,class="num">1,(HORIZON*class="num">2),ma_o);
   CopyBuffer(atr_handle,class="num">0,class="num">0,class="num">1,atr);
   ArraySetAsSeries(ma_c,true);
   ArraySetAsSeries(ma_o,true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Manage The Trade We Have Open                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void manage_setup(class="type">void)
  {
   if((PositionsTotal() > class="num">0) && (holding_period < (HORIZON-class="num">1)))
      holding_period +=class="num">1;
   else

◍ 把K线与均线差喂给双模型再开仓

策略在 find_setup 里先把当前品种、当前周期的基础行情和跨 HORIZON 根K线的差值塞进 10 维向量,再跑两个 ONNX 模型拿信号。模型输入固定为 10 个浮点,输出各 1 个浮点,任一模型推理失败就用 Comment 在图表左上角报错,不会静默跳过。 具体喂给 model_inputs 的字段:0~3 分别是当根开高低收,4~7 是当根开高低收减去 HORIZON 根之前对应值,8 是本根收盘均线 ma_c[0],9 是 ma_c[0] 与 ma_c[HORIZON] 的差。这样模型既看绝对价也看区间位移和均线斜率。 开仓条件卡得很死:model_outputs[0] 必须大于 0,同时 ma_c[0] 要小于 ma_o[0](收盘均线在开盘均线下方,倾向弱势反弹架构),并且当根收盘价站上 ma_c[0],才用 Trade.Buy 以 ask 价吃单,手数由 TRADING_VOLUME 控制、不挂止损止盈。外汇与贵金属杠杆高,这种裸仓策略回撤可能很陡,真上 MT5 前建议先开可视化回测把 HORIZON 和模型阈值调一遍。

MQL5 / C++
  if((PositionsTotal() > class="num">0) && (holding_period == (HORIZON - class="num">1)))
      Trade.PositionClose(Symbol());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Find A Trading Oppurtunity For Our Strategy                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void find_setup(class="type">void)
  {
   vectorf model_inputs   = vectorf::Zeros(class="num">10);
   vectorf model_outputs  = vectorf::Zeros(class="num">1);
   vectorf model_2_outputs = vectorf::Zeros(class="num">1);
   holding_period = class="num">0;
   class="type">int i = class="num">0;
   model_inputs[class="num">0] = (class="type">float) iOpen(Symbol(),PERIOD_CURRENT,class="num">0);
   model_inputs[class="num">1] = (class="type">float) iHigh(Symbol(),PERIOD_CURRENT,class="num">0);
   model_inputs[class="num">2] = (class="type">float) iLow(Symbol(),PERIOD_CURRENT,class="num">0);
   model_inputs[class="num">3] = (class="type">float) iClose(Symbol(),PERIOD_CURRENT,class="num">0);
   model_inputs[class="num">4] = (class="type">float)(iOpen(Symbol(),PERIOD_CURRENT,class="num">0) - iOpen(Symbol(),PERIOD_CURRENT,HORIZON));
   model_inputs[class="num">5] = (class="type">float)(iHigh(Symbol(),PERIOD_CURRENT,class="num">0) - iHigh(Symbol(),PERIOD_CURRENT,HORIZON));
   model_inputs[class="num">6] = (class="type">float)(iLow(Symbol(),PERIOD_CURRENT,class="num">0) - iLow(Symbol(),PERIOD_CURRENT,HORIZON));
   model_inputs[class="num">7] = (class="type">float)(iClose(Symbol(),PERIOD_CURRENT,class="num">0) - iClose(Symbol(),PERIOD_CURRENT,HORIZON));
   model_inputs[class="num">8] = (class="type">float) ma_c[class="num">0];
   model_inputs[class="num">9] = (class="type">float)(ma_c[class="num">0] - ma_c[HORIZON]);
   if(!OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_outputs))
     {
       Comment("Failed to run the ONNX model correctly.");
     }
   if(!OnnxRun(onnx_model_2,ONNX_DEFAULT,model_inputs,model_2_outputs))
     {
       Comment("Failed to run the ONNX model correctly.");
     }
   if(model_outputs[class="num">0] > class="num">0)
     {
       if(ma_c[class="num">0] < ma_o[class="num">0])
         {
           if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) > ma_c[class="num">0])
               Trade.Buy(TRADING_VOLUME,Symbol(),ask,class="num">0,class="num">0,"");
         }
       else

双模型信号下的均线交叉下单逻辑

这段代码片段展示了在第二个模型输出为正时,如何依据收盘线与双均线(ma_c 与 ma_o)的相对位置触发买卖。逻辑上把「模型判断」和「价格回踩均线」做了两层过滤,不是模型一响就直接进场。 具体看:当 ma_c[0] > ma_o[0] 且当前收盘价低于 ma_c[0],执行 Sell;当 ma_c[0] < ma_o[0] 且收盘价低于 ma_c[0],执行 Buy;若 ma_c[0] > ma_o[0] 且收盘价高于 ma_c[0],再走一次 Sell。也就是说,同向均线排列下价格未脱离均线才允许反向开仓,倾向把假突破挡在门外。 末尾用 Comment 把两个模型输出打印到图表,方便肉眼比对信号衰减;最后一组 #undef 把 HORIZON、MA_MODE、MA_PERIOD、MA_SHIFT、TRADING_VOLUME 全部取消定义,避免多文件编译时的宏污染。外汇与贵金属杠杆高,这类条件单在滑点行情中可能频繁触发,上 MT5 用策略测试器跑 EURUSD 的 M15 先验证信号密度再考虑实盘。

MQL5 / C++
if(ma_c[class="num">0] > ma_o[class="num">0])
  {
    if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) < ma_c[class="num">0])
      Trade.Sell(TRADING_VOLUME,Symbol(),bid,class="num">0,class="num">0,"");
  }
}
else
  if(model_2_outputs[class="num">0] > class="num">0)
  {
    if(ma_c[class="num">0] < ma_o[class="num">0])
      {
        if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) < ma_c[class="num">0])
          Trade.Buy(TRADING_VOLUME,Symbol(),ask,class="num">0,class="num">0,"");
      }
    if(ma_c[class="num">0] > ma_o[class="num">0])
      {
        if(iClose(Symbol(),PERIOD_CURRENT,class="num">0) > ma_c[class="num">0])
          Trade.Sell(TRADING_VOLUME,Symbol(),bid,class="num">0,class="num">0,"");
      }
  }
Comment("class="num">0: ",model_outputs[class="num">0],"class="num">1: ",model_2_outputs[class="num">0]);
}
class=class="str">"cmt">//#------------------------------------------------------------------+
class=class="str">"cmt">//#| Undefine system constants                                        |
class=class="str">"cmt">//#------------------------------------------------------------------+
class="macro">#undef HORIZON
class="macro">#undef MA_MODE
class="macro">#undef MA_PERIOD
class="macro">#undef MA_SHIFT
class="macro">#undef TRADING_VOLUME
class=class="str">"cmt">//#------------------------------------------------------------------+

「最后一句大实话」

把 RMSE 当唯一尺子挑模型,等于让 EA 在回测里合法作弊——它只会收敛到市场平均收益附近,对外汇和贵金属这种高波动、高杠杆品种,意味着实盘可能直接撞上未知回撤。 附件里的 EURUSD_DRS.mq5 和两个 .onnx 模型(趋势跟踪 7137.58 KB、均值回归常驻)给了可直接加载的验证素材,先跑一遍 DRS 切换逻辑,别裸奔上实盘。 真要挖超额收益,得用 TSS 跟 RMSE 做相对比较来筛市场低效性,而不是迷信误差最小化。本文演示的验证流程复现一次,比读十篇综述都管用。

常见问题

用双模型对冲:一个按你的逻辑训练,一个随机或对照训练,两者预测持续背离且你那个不占优,就说明理解力有漏洞。
在系统骨架里先定好常量与依赖,写循环把每根K线减去均线的值直接灌进数组落盘,别一根根手填。
可以,小布能按你给的标注规则做双状态切分并跑独立模型比对,把背离结果直接在品种页列出来。
用价格相对均线的偏离幅度和持续时间两个阈值切分,偏离超阈且延续算趋势态,否则震荡态,分开写标签。
把双模型预测导出成通用交换格式(如ONNX),确认输入输出维度与终端接收结构一致,避免加载报错。