突破机器学习的局限(第一部分):缺乏可互操作的度量指标·综合运用
用双模型对冲检验市场理解力
前文已说明,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;此类品种杠杆高,实盘前务必用历史数据回测确认字段含义。
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 策略测试器跑一次看文件行数是否等于样本柱数。外汇与贵金属杠杆高,这类特征工程只是给模型喂料,信号失效概率不低。
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 重跑这段标注逻辑。
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 周期,确认两模型加权输出和价格变动方向吻合再考虑接实盘。
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)显示盈利期与亏损期交替——模型没预判波动率,不过策略有自我修正、倾向回到预期收益轨道。外汇和贵金属杠杆高,这套机制只是概率优势,实盘前请在策略测试器里自己跑一遍验证。
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 数据验证加载耗时与信号延迟。
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] 是最近一根均值。外汇与贵金属杠杆高,这类周期参数错配会直接放大滑点风险。
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 和模型阈值调一遍。
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 先验证信号密度再考虑实盘。
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 做相对比较来筛市场低效性,而不是迷信误差最小化。本文演示的验证流程复现一次,比读十篇综述都管用。