Python中的虚假回归(伪回归)·综合运用
(3/3)· 把前两步的检验与建模收口到交易端,看伪回归模型如何被真实行情拆穿
把欧元美元模型落地成 ONNX 双精度文件
ONNX 是一套开放模型交换格式,核心作用是让训练好的机器学习模型脱离原框架,在支持该格式的平台间直接迁移。对 MT5 交易者来说,这意味着用 Python 训好的线性回归模型,可以转成文件后交给其他推理端调用,不必把整套训练环境搬进终端。 下面这段脚本干的事很具体:先把输入特征列数从 DataFrame 里取出来,定义成双精度张量;再把 sklearn 的线性回归模型 lm 转成 ONNX,运算符集锁在 12 版;最后存成 EURUSD_ONNX_Double.onnx。特征维度用 csv.loc[:,predictors].shape[1] 动态决定,第一维 None 表示样本数不固定,适合按批推理。 双精度(DoubleTensorType)在这里不是凑字样。外汇价格本身小数位多,若用单精度导出,EURUSD 这类品种在 1.0800 附近的微小波动可能在推理端被截断,回测与实盘信号容易漂移。 实盘接这套文件前,先在 Python 侧用 onnxruntime 跑一遍相同样本,比对输出与 sklearn 原生 predict 的误差;外汇与贵金属杠杆高、滑点随机,模型互换格式只是工程环节,不预示任何方向胜率。
initial_type_double = [(&class="macro">#x27;double_input&class="macro">#x27;, DoubleTensorType([None, csv.loc[:,predictors].shape[class="num">1]]))] onnx_model_double = convert_sklearn(lm, initial_types=initial_type_double, target_opset=class="num">12) onnx_model_filename = "EURUSD_ONNX" onnx_filename=onnx_model_filename+"_Double.onnx" onnx.save_model(onnx_model_double, onnx_filename)
「用 MetaEditor 把 ONNX 模型塞进 EA 骨架」
在 MT5 里做自动化交易,EA 本质是一段 MQL5 脚本,难点不在下单逻辑,而在把训练好的机器学习模型接进来。MetaEditor 自带编译和调试环境,能直接把 ONNX 文件作为资源嵌入,省去运行时外部读取的麻烦。 代码开头先 #include <Trade\Trade.mqh>,这是 MT5 处理开平仓的标准库。引入 CTrade 对象后,后续下单、改止损都走预定义方法,不用自己拼交易事件结构。 #resource 指令把 EURUSD_ONNX_Double.onnx 以 uchar 数组形式编进 EA,运行时用 ONNXHandle 持有模型句柄,初始置 INVALID_HANDLE,等加载成功才被改写。PredictedMove 这类预测变量先给 -1,代表模型还没吐结果,后面跑完推理再覆盖。 指标端声明了 ma_handle / ma_reading[] 和 ao_handle / ao_reading[],分别接移动平均线和 Awesome 振荡器。句柄拿数据、数组存历史值,EA 就能在每根 K 线拿它们和模型输出交叉验证。 注意输入参数里 sl_width=150 是点数止损宽度,positions=1 控制同时开仓数,lot_multiple=1 表示按最小手数倍数下量。外汇和贵金属杠杆高,模型预测仅提供概率倾向,实盘前务必在策略测试器用历史数据跑通。
class=class="str">"cmt">//Our trade class helps us open trades class="macro">#include <Trade\Trade.mqh> CTrade Trade; class=class="str">"cmt">//Loading our ONNX model class="macro">#resource "ONNX\\EURUSD_ONNX_Double.onnx" as class="type">uchar EURUSD_ONNX_MODEL[] class="type">long ONNXHandle=INVALID_HANDLE; class=class="str">"cmt">//Handles for our technical indicators and dynamic arrays to store their readings class="type">int ma_handle; class="type">class="kw">double ma_reading[]; class="type">int ao_handle; class="type">class="kw">double ao_reading[]; class=class="str">"cmt">//Inputs class="type">int class="kw">input sl_width = class="num">150; class=class="str">"cmt">//How wide should the stoploss be? class="type">int class="kw">input positions = class="num">1; class=class="str">"cmt">//How many positions should the we open? class="type">int class="kw">input lot_multiple = class="num">1; class=class="str">"cmt">//How many times greater than minimum lot should each position be? class=class="str">"cmt">//Symbol variables class="type">class="kw">double min_volume; class="type">class="kw">double bid,ask; class=class="str">"cmt">//We&class="macro">#x27;ll use this time stamp to keep track of the number of candles passing class="kw">static class="type">class="kw">datetime time_stamp; class=class="str">"cmt">//Our model&class="macro">#x27;s forecast will be stored here vector model_forecast(class="num">1);
◍ EA 挂载时 OnInit 做了什么
把 EA 拖到 MT5 图表上的一瞬间,OnInit() 就会跑一遍,它决定这个机器人能不能活下来。我们这段逻辑强制只认 EURUSD 的 M1 周期:不是这个品种或周期,直接 Print 提示并 return(INIT_FAILED),初始化当场中断。 接着从静态缓冲区 ONNXModel 里用 OnnxCreateFromBuffer 建模型,拿到 ONNXHandle。若等于 INVALID_HANDLE,把 GetLastError() 打出来再失败返回——这一步没过,后面机器学习推理全免谈。 输入张量形状写死为 {1,5},即一行五列;输出为 {1,1},单个标量。两者分别用 OnnxSetInputShape / OnnxSetOutputShape 校验,任一失败同样 INIT_FAILED。这里 5 个输入通常对应 MA、AO 等特征拼接,输出 1 维多为方向概率。 最后挂两个 M1 指标:20 周期收盘价 EMA(iMA 句柄给 ma_handle)和 Awesome 振荡器(iAO 句柄给 ao_handle),再取经纪商最小合约 min_volume。全部就绪才 return(INIT_SUCCEEDED)。 别把品种周期写死当万能 真要换品种测,至少把 "EURUSD" 和 PERIOD_M1 两处判断抽成外部输入,否则换图表必 INIT_FAILED,排查半天还以为模型坏了。
class="type">int OnInit(){ class=class="str">"cmt">//Validating trading conditions if(_Symbol!="EURUSD" || _Period!=PERIOD_M1) { Print("Model must work with EURUSD on the M1 timeframe"); class="kw">return(INIT_FAILED); } ONNXHandle=OnnxCreateFromBuffer(ONNXModel,ONNX_DEFAULT); if(ONNXHandle==INVALID_HANDLE) { Print("OnnxCreateFromBuffer error ",GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//Defining the model&class="macro">#x27;s class="kw">input shape const class="type">long input_shape[] = {class="num">1,class="num">5}; if(!OnnxSetInputShape(ONNXHandle,ONNX_DEFAULT,input_shape)) { Print("OnnxSetInputShape error ",GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//Defining the model&class="macro">#x27;s output shape const class="type">long output_shape[] = {class="num">1,class="num">1}; if(!OnnxSetOutputShape(ONNXHandle,class="num">0,output_shape)) { Print("OnnxSetOutputShape error ",GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//Setting up our technical indicators ma_handle = iMA(_Symbol,PERIOD_M1,class="num">20,class="num">0,MODE_EMA,PRICE_CLOSE); ao_handle = iAO(_Symbol,PERIOD_M1); min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); class="kw">return(INIT_SUCCEEDED); }
EA卸载时怎么干净释放ONNX资源
MT5 的 EA 在被手动移除、切换周期或终端关闭时,会触发 OnDeinit。这一步若偷懒不清理,加载过的 ONNX 模型句柄会一直占着内存,多次重载策略后终端变卡的概率明显上升。 看代码逻辑:先判断 ONNXHandle 是否不等于 INVALID_HANDLE。若模型此前已初始化,就调 OnnxRelease 把模型资源交还系统,并立刻把句柄置回 INVALID_HANDLE,避免后续误用悬空指针。 最后一行 ExpertRemove 让 EA 自身退出。整套动作不复杂,但能直接压住内存泄漏,属于写 AI 策略 EA 时不能省的底子工程。外汇与贵金属杠杆高,模型崩在资源没清干净的环境里,复盘都难定位。
class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//OnDeinit we should freeup resources we don&class="macro">#x27;t require class=class="str">"cmt">//We&class="macro">#x27;ll begin by releasing the ONNX models then removing the Expert Advisor if(ONNXHandle!=INVALID_HANDLE) { OnnxRelease(ONNXHandle); ONNXHandle=INVALID_HANDLE; } class=class="str">"cmt">//Lastly remove the Expert Advisor ExpertRemove(); }
「行情Tick里喂给模型的最新指标快照」
EA 里 OnTick 是每次收到新报价就触发的入口。想在外汇或贵金属这种高波动品种上做实时机器学习预测,就得在这里把指标数据刷成最新,否则模型看到的永远是旧盘面。 具体做法是用 CopyBuffer 从 MA 和 AO 两个指标句柄各取最近 10 个值进数组,再用 ArraySetAsSeries 把数组倒序——索引 0 是当前值,越往后越老。这样后面写逻辑不用自己算偏移,直接 ma_reading[0] 就是眼前这根。 时间锚点用 iTime(_Symbol, PERIOD_M1, 0) 拿当前 M1 柱时间。只要它和上次存的 time_stamp 不同,就说明来了新柱,此时若无持仓调 ModelForecast 让模型出信号,有持仓则走 ManageTrade 管风控。注意外汇/贵金属杠杆高,模型误判可能瞬间放大亏损,这段代码只解决「何时喂数」,不保证信号对。 下面这段是 MT5 里可直接贴进 EA 的 OnTick 骨架,逐行拆完你能马上改参数跑: void OnTick() 定义每次报价执行的函数体 // 前两行注释:把技术指标数组更新到当前值,并设为序列方便取最新 CopyBuffer(ma_handle,0,0,10,ma_reading); 从 MA 句柄取 0~9 共 10 个缓冲写进 ma_reading ArraySetAsSeries(ma_reading,true); 把 ma_reading 设为时间序列,索引 0 为最新 CopyBuffer(ao_handle,0,0,10,ao_reading); 同上取 AO 振荡器最近 10 值 ArraySetAsSeries(ao_reading,true); AO 数组也倒序 ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); 取当前品种卖一价 bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); 取买一价 // Update time marker 注释:更新时间标记 datetime current_time = iTime(_Symbol, PERIOD_M1, 0); 取本品种 M1 当前柱开盘时间 if(time_stamp != current_time){ 若时间和上次不同,即新 K 线 // No open positions 注释分支 if(PositionsTotal() == 0){ 若无任何持仓 ModelForecast(); 调用模型预测函数 } // We have open positions to manage 注释分支 else if(PositionsTotal() > 0){ 若有持仓 ManageTrade(); 调用持仓管理函数 } } 把 10 改成 20 就能让模型看更长窗口,但 Tick 内复制开销随数组变长线性增加,实盘前建议在 MT5 策略测试器数一下单 Tick 耗时。
class="type">void OnTick() { class=class="str">"cmt">//Update the arrays storing the technical indicator values to their current values class=class="str">"cmt">//Then set the arrays as series so that the current reading is at the top and the oldest reading is last CopyBuffer(ma_handle,class="num">0,class="num">0,class="num">10,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(ao_handle,class="num">0,class="num">0,class="num">10,ao_reading); ArraySetAsSeries(ao_reading,true); ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); class=class="str">"cmt">//Update time marker class="type">class="kw">datetime current_time = iTime(_Symbol,PERIOD_M1,class="num">0); class=class="str">"cmt">//Periodically make forecasts if we have no open positions if(time_stamp != current_time){ class=class="str">"cmt">//No open positions if(PositionsTotal() == class="num">0){ ModelForecast(); } class=class="str">"cmt">//We have open positions to manage else if(PositionsTotal() > class="num">0){ ManageTrade(); } } }
◍ EA 里跑 ONNX 推理的那几行
把训练好的 ONNX 模型接进 MT5 专家顾问,核心就是 ModelForecast 这一个 void 函数。它不接收参数,只在内部拼好输入向量、调 OnnxRun、把结果丢给下一步。 输入特征固定为 5 个:当前 M1 K 线的高、低、收,以及 ma_reading[0](20 周期 EMA 收盘价)和 ao_reading[0](Awesome 振荡器)。注意周期写死在 PERIOD_M1,换周期得改代码而非传参。 OnnxRun 用了 ONNX_NO_CONVERSION 标志,意味着推理时不自动转数据类型,输入向量必须是模型期望的精度,否则会进 if 分支打错误码。推理失败只 Print 不中断,成功才顺带调用 NextMove() 推进交易逻辑。 别把正态当圣经:这段没有任何风控或阈值判断,model_forecast 打印出来后怎么用完全在 NextMove 里。外汇与贵金属杠杆高、模型误判概率不低,实盘前务必在策略测试器用历史 Tick 验证输出分布。
class=class="str">"cmt">//This function provides these utilities: class=class="str">"cmt">// class="num">1) Inferencing using our ONNX model class=class="str">"cmt">// class="num">2) Calling the next function responsible for intepreting model forecasts and other subroutines class="type">void ModelForecast(class="type">void){ class=class="str">"cmt">//These are the inputs for our ONNX model: class=class="str">"cmt">// class="num">1)High class=class="str">"cmt">// class="num">2)Low class=class="str">"cmt">// class="num">3)Close class=class="str">"cmt">// class="num">4)class="num">20 PERIOD MA MODE: EMA APPLIED_PRICE:PRICE CLOSE class=class="str">"cmt">// class="num">5)Awesome Oscilator vector model_input{iHigh(_Symbol,PERIOD_M1,class="num">0),iLow(_Symbol,PERIOD_M1,class="num">0),iClose(_Symbol,PERIOD_M1,class="num">0),ma_reading[class="num">0],ao_reading[class="num">0]}; class=class="str">"cmt">//Inferencing with our model if(!OnnxRun(ONNXHandle,ONNX_NO_CONVERSION,model_input,model_forecast)){ Print("Error performing inference: ",GetLastError()); } class=class="str">"cmt">//Pring model forecast to the terminal else{ Print(model_forecast); NextMove(); } }
预测输出怎么被翻译成下单动作
模型跑完之后不会自己下单,得有人把那一串 forecast 数值读成「涨过现价」或「跌过现价」的判定。NextMove 干的就是这个翻译兼调度:它先后用方向参数 1 和 -1 去问 InterpretForecast,拿到 true 才去调 CheckOrder,方向错了就换另一边试。 InterpretForecast 的逻辑很硬:只认 model_forecast[0] 这一个元素,direction=1 时要求它大于 1,direction=-1 时要求它小于 1,其他任何入参直接返回 false。也就是说模型输出若不在「明显大于/小于 1」的区间里,这套解析就当没看见。 外汇与贵金属波动剧烈、滑点随机,这种非黑即白的阈值判断可能漏掉大量模糊信号;实盘前建议在 MT5 里把 model_forecast[0] 的分布打印出来,确认大于 1 和小于 1 的样本占比,避免函数长期返回 false 而从不触发。 下面这段是原始实现,逐行看更清楚调度关系:NextMove 先查多头解释,命中就 CheckOrder(1) 并 return;不命中再查空头解释,命中才 CheckOrder(-1)。InterpretForecast 里两个 if 分别管正负方向,末尾 false 是兜底。
class=class="str">"cmt">//This function provides these utilities: class=class="str">"cmt">// class="num">1) Getting the model output intepreted class=class="str">"cmt">// class="num">2) Acting on the intepretations class="type">void NextMove(){ if(InterpretForecast(class="num">1)){ CheckOrder(class="num">1); class="kw">return; } else if(InterpretForecast(-class="num">1)){ CheckOrder(-class="num">1); } } class=class="str">"cmt">//This function provides these utilities: class=class="str">"cmt">// class="num">1) Check whether the model forecasted a reading greater than or less than class="num">1. class="type">bool InterpretForecast(class="type">int direction){ class=class="str">"cmt">//class="num">1 means check if the model is forecasting growth greater than the current price if(direction == class="num">1){ class="kw">return(model_forecast[class="num">0] > class="num">1); } class=class="str">"cmt">//-class="num">1 means check if the model is forecasting growth less than the current price if(direction == -class="num">1){ class="kw">return(model_forecast[class="num">0] < class="num">1); } class=class="str">"cmt">//Otherwise class="kw">return class="kw">false. class="kw">return class="kw">false; }
「无持仓时的定向开仓与动态止损止盈」
EA 把开仓逻辑收敛进 CheckOrder 函数:只有当账户里 PositionsTotal() 返回 0,也就是当前没有任何未平头寸时,才允许按 order_direction 参数发单。order_direction 传 1 走买入分支,传 -1 走卖出分支,两个分支都再用一次 PositionsTotal()==0 做冗余校验,避免同根信号重复触发。 开仓不是一次只下一单。代码里用 for 循环跑 positions 次,每次调用 Trade.PositionOpen,手数固定为 min_volume * lot_multiple,买用 ask 价、卖用 bid 价,注释标了魔法号 "Volatitlity Doctor AI"。你在 MT5 策略测试器里把 positions 设成 3,就可能看到同一信号下连续铺 3 个同方向头寸。 持仓后的风控交给 ManageTrade,它只做一件事——调 CheckStop。CheckStop 倒序遍历所有持仓,用 PositionGetSymbol 取每个仓的符号,先卡一道 _Symbol == symbol,不是当前图表品种的头寸一律跳过。 通过品种校验后,函数按 sl_width 和点值 _Point 算新止损止盈:买仓基于卖价重算、卖仓基于买价重算,且只在“新水平比原有更优”时才改。NormalizeDouble 按 _Digits 修精度,确有必要才 Trade.PositionModify。外汇与贵金属杠杆高,这类自动挪损盈在跳空时可能失效,实盘前先用历史数据回测确认。
class=class="str">"cmt">//This function is responsible for opening positions class="type">void CheckOrder(class="type">int order_direction){ class=class="str">"cmt">//Only open new positions if we have no open positions if(PositionsTotal() == class="num">0){ class=class="str">"cmt">//Buy if(order_direction == class="num">1){ class=class="str">"cmt">//Iterate over the desired number of positions for(class="type">int i = class="num">0; i < positions; i++){ Trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,min_volume * lot_multiple,ask,class="num">0,class="num">0,"Volatitlity Doctor AI"); } } class=class="str">"cmt">//Sell else if(order_direction == -class="num">1 && PositionsTotal() == class="num">0){ class=class="str">"cmt">//Iterate over the desired number of positions for(class="type">int i = class="num">0; i < positions; i++){ Trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,min_volume * lot_multiple ,bid,class="num">0,class="num">0,"Volatitlity Doctor AI"); } } } } class=class="str">"cmt">//This function handles our trade management class="type">void ManageTrade(){ CheckStop(); } class=class="str">"cmt">//This funciton will update our S/L & T/P class="type">void CheckStop(){ class=class="str">"cmt">//First we iterate over the total number of open positions for(class="type">int i = PositionsTotal() -class="num">1; i >= class="num">0; i--){ class=class="str">"cmt">//Then we fetch the name of the symbol of the open position class="type">class="kw">string symbol = PositionGetSymbol(i); class=class="str">"cmt">//Before going any furhter we need to ensure that the symbol of the position matches the symbol we&class="macro">#x27;re trading if(_Symbol == symbol){ class=class="str">"cmt">//Now we get information about the position
◍ 多单尾仓的止损挪移逻辑
在 MT5 的持仓遍历循环里,先通过 PositionGetInteger 拿到 ticket 与持仓类型,再用 PositionGetDouble 取开仓价、当前 SL。这套取值是后续所有修改动作的前提,缺一个都会让 PositionModify 直接报错。 针对多单分支,新止损放在 ask 减去一半 sl_width 点差的位置,新止盈则是 ask 加上完整 sl_width 点差,统一用 NormalizeDouble 按 _Digits 截断。例如 sl_width=200、_Point=0.00001 的黄金品种,新 SL 距 ask 约 1 点、新 TP 距 ask 约 2 点。 修改触发条件很硬:仅当原 SL 小于算出值,或原 SL 为 0(市价单未设损)才执行。这样能避免把已有更宽止损往不利方向收,外汇与贵金属杠杆高,乱移损可能瞬间放大回撤风险。 别让旧损被覆盖 写 EA 时很多人忽略 current_stop_loss == 0 的判定,结果把手动设好的宽损给洗掉。建议先在策略测试器用欧美 1 分钟数据跑一遍,确认日志里 Modify 次数符合预期再上模拟盘。
class="type">ulong ticket = PositionGetInteger(POSITION_TICKET); class=class="str">"cmt">//Position Ticket class="type">class="kw">double position_price = PositionGetDouble(POSITION_PRICE_OPEN); class=class="str">"cmt">//Position Open Price class="type">long type = PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//Position Type class="type">class="kw">double current_stop_loss = PositionGetDouble(POSITION_SL); class=class="str">"cmt">//Current Stop loss value class=class="str">"cmt">//If the position is a buy if(type == POSITION_TYPE_BUY){ class=class="str">"cmt">//The new stop loss value is just the ask price minus the stop we calculated above class="type">class="kw">double new_stop_loss = NormalizeDouble(ask - ((sl_width * _Point) / class="num">2) ,_Digits); class=class="str">"cmt">//The new take profit is just the ask price plus the stop we calculated above class="type">class="kw">double new_take_profit = NormalizeDouble(ask + (sl_width * _Point),_Digits); class=class="str">"cmt">//If our current stop loss is less than our calculated stop loss class=class="str">"cmt">//Or if our current stop loss is class="num">0 then we will modify the stop loss and take profit if((current_stop_loss < new_stop_loss) || (current_stop_loss == class="num">0)){ Trade.PositionModify(ticket,new_stop_loss,new_take_profit); } } class=class="str">"cmt">//If the position is a sell else if(type == POSITION_TYPE_SELL){ class=class="str">"cmt">//The new stop loss value is just the ask price minus the stop we calculated above
挂单改止损的对称算法
这段逻辑处理的是持仓止损与止盈的对称重设:以 bid 为基准,止损放在 bid 上方 sl_width 一半的点距处,止盈则落在 bid 下方一个完整 sl_width 点距处。用 NormalizeDouble 按 _Digits 精度截断,避免跨品种点值误差。 判定条件很直接——当当前止损大于新算出的止损,或当前止损为 0(即未设防),就调用 PositionModify 把两个价位一起改掉。外汇与贵金属杠杆高,这类自动改单逻辑在跳空时可能失效,实盘前务必在 MT5 策略测试器用历史 tick 跑一遍。 代码逐行拆解: new_stop_loss = NormalizeDouble(bid + ((sl_width * _Point)/2), _Digits); // 止损 = 买价 + 半宽点距,按小数位规范化 double new_take_profit = NormalizeDouble(bid - (sl_width * _Point), _Digits); // 止盈 = 买价 - 全宽点距,规范化
| if((current_stop_loss > new_stop_loss) | (current_stop_loss == 0)){ // 原止损过宽或未设 |
|---|
Trade.PositionModify(ticket,new_stop_loss,new_take_profit); // 改单 }
class="type">class="kw">double new_stop_loss = NormalizeDouble(bid + ((sl_width * _Point)/class="num">2),_Digits); class=class="str">"cmt">//The new take profit is just the ask price plus the stop we calculated above class="type">class="kw">double new_take_profit = NormalizeDouble(bid - (sl_width * _Point),_Digits); class=class="str">"cmt">//If our current stop loss is greater than our calculated stop loss class=class="str">"cmt">//Or if our current stop loss is class="num">0 then we will modify the stop loss and take profit if((current_stop_loss > new_stop_loss) || (current_stop_loss == class="num">0)){ Trade.PositionModify(ticket,new_stop_loss,new_take_profit); } } } } }
「最后一句大实话」
做时间序列回归最坑的就是伪回归,两个本来各走各的非平稳序列,凑一起算出的高 R² 往往是假象,EURUSD 日线用 FetchData.mq5 拉 330KB 数据回测就常出现这种虚高拟合。 真要落地,先跑单位根检验看平稳性,不平稳就上协整或误差修正模型;文末给的 Spurious_Regression_ONNX_EA.mq5(10.16KB)能直接挂 MT5 验证 ONNX 双精度模型在样本内外的差异。 外汇和贵金属杠杆高、序列非平稳是常态,任何统计套利结论都只是概率倾向,开实盘前务必用未见过的数据重测残差。