克服机器学习的局限性(第二部分):缺乏可重复性·进阶篇
📘

克服机器学习的局限性(第二部分):缺乏可重复性·进阶篇

第 2/2 篇

◍ 用零策略基准挑出更好预测的经纪商

两家经纪商的欧元兑美元报价在大多数时段几乎独立波动,但预测未来10日收益率的难度并不对称。我们引入 TSS(零策略标准)作为底线:把所有输入列刻意填成 0,线性模型只能吐出目标均值,也就是「永远押市场平均收益」的笨办法误差。任何正经模型若连这都超不过,说明根本没有预测能力。 用 TimeSeriesSplit 做 5 折时序交叉验证,且 train_test_split 必须 shuffle=False,避免未来信息泄漏。代码里分别对经纪商 A、B 算 TSS 与真实 RSS 的比值,结果显示经纪商 B 的 RSS/TSS 明显更低——在该样本上超越基准更容易,意味着换一家经纪商,EURUSD 的十日均线收益效率可能变了。 确定聚焦经纪商 B 后,用 GradientBoostingRegressor 拟合其全部输入,并导成 ONNX 丢进 MT5 用。外汇与贵金属属高风险品种,跨经纪商数据差异会直接改写模型胜率,实盘前请在策略测试器里重跑这段代码核对自己数据源的 RSS/TSS。

MQL5 / C++
from sklearn.model_selection class="kw">import train_test_split,TimeSeriesSplit,cross_val_score
from sklearn.linear_model class="kw">import Ridge
tscv = TimeSeriesSplit(n_splits=class="num">5,gap=HORIZON)
def get_model():
    class="kw">return(Ridge())
train , test = train_test_split(combined,shuffle=False,test_size=class="num">0.5)
broker_a_tss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,[&class="macro">#x27;Null&class="macro">#x27;]],train.loc[:,&class="macro">#x27;A Target&class="macro">#x27;],scoring=&class="macro">#x27;neg_mean_squared_error&class="macro">#x27;,n_jobs=-class="num">1,cv=tscv)))
broker_a_rss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,inputs[class="num">0:(len(inputs)class=class="str">"cmt">//class="num">2)]],train.loc[:,&class="macro">#x27;A Target&class="macro">#x27;],scoring=&class="macro">#x27;neg_mean_squared_error&class="macro">#x27;,n_jobs=-class="num">1,cv=tscv)))
broker_b_tss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,[&class="macro">#x27;Null&class="macro">#x27;]],train.loc[:,&class="macro">#x27;B Target&class="macro">#x27;],scoring=&class="macro">#x27;neg_mean_squared_error&class="macro">#x27;,n_jobs=-class="num">1,cv=tscv)))
broker_b_rss = np.mean(np.abs(cross_val_score(get_model(),train.loc[:,inputs[(len(inputs)class=class="str">"cmt">//class="num">2):]],train.loc[:,&class="macro">#x27;B Target&class="macro">#x27;],scoring=&class="macro">#x27;neg_mean_squared_error&class="macro">#x27;,n_jobs=-class="num">1,cv=tscv)))
res = [(broker_a_rss/broker_a_tss),(broker_b_rss/broker_b_tss)]
eff = str(((res[class="num">0] - res[class="num">1])/res[class="num">1]) * class="num">100)
t = &class="macro">#x27;The EURUSD Appears &class="macro">#x27; + eff[class="num">0:class="num">4] + &class="macro">#x27;% Easier To Forecast With Broker B&class="macro">#x27;
sns.barplot(res,class="type">color=&class="macro">#x27;black&class="macro">#x27;)
plt.axhline(np.min(res),class="type">color=&class="macro">#x27;red&class="macro">#x27;,linestyle=&class="macro">#x27;:&class="macro">#x27;)
plt.ylabel(&class="macro">#x27;class="num">5-Fold Cross Valiated Ratio of RSS/TSS &class="macro">#x27;)
plt.title(t)
plt.xticks([class="num">0,class="num">1],[&class="macro">#x27;Broker A&class="macro">#x27;,&class="macro">#x27;Broker B&class="macro">#x27;])
b_inputs = inputs[len(inputs)class=class="str">"cmt">//class="num">2:]
from sklearn.ensemble class="kw">import GradientBoostingRegressor
model = GradientBoostingRegressor()
model.fit(train.loc[:,b_inputs[:-class="num">2]],train[&class="macro">#x27;B Target&class="macro">#x27;])
class="kw">import skl2onnx,onnx
initial_types = [(&class="macro">#x27;float_input&class="macro">#x27;,skl2onnx.common.data_types.FloatTensorType([class="num">1,class="num">4]))]
onnx_proto = skl2onnx.convert_sklearn(model,initial_types=initial_types,target_opset=class="num">12)
onnx.save(onnx_proto,"EURUSD GBR D1.onnx")

同一套模型换经纪商就失灵

把 ONNX 模型打进 MT5 程序的第一步,是在编译期用 #resource 把模型文件一起打包,再定义全局系统常量。原文示例里写死了 10 日收益周期(RETURN_PERIOD 10)、D1 周期和 4 维输入 1 维输出,这些常量直接决定了策略回看窗口和推理形状。 初始化时加载 ONNX 句柄、重置全局变量;退出时务必释放模型资源,否则内存泄漏会拖垮终端。价格更新后每天只记一次收盘价,无持仓就跑模型预测开仓,有持仓则在 10 日持有期内尽量用跟踪止损。 回测用了样本外日期,且对两家经纪商 A、B 保持完全一致的设置。经纪商 B 是开发端用的,经纪商 A 模拟客户部署端。图14 里 B 端资金曲线明显正向、样本外拟合不错;但图15 里同样参数丢到 A 端,余额上升完全没复现。 别把经纪商差异当噪声。EURUSD 在两家点差、报价精度、最小止损距离上的细微不同,就足以让没重训练的 GBR 模型从赚钱变白干。外汇和贵金属本身高杠杆高风险,跨经纪商直接搬策略大概率翻车,开发者没法为全球每家经纪商单独定制模型。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 EURUSD.mq5 |
class=class="str">"cmt">//|                                                       Gamuchirai Ndawana |
class=class="str">"cmt">//|                                        [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Gamuchirai Ndawana"
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 Definitions                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include  <Trade\Trade.mqh>
CTrade Trade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| System Constants Definitions                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define  ONNX_INPUT_SHAPE class="num">4
class="macro">#define  ONNX_OUTPUT_SHAPE class="num">1
class="macro">#define  SYSTEM_TIME_FRAME PERIOD_D1
class="macro">#define  RETURN_PERIOD class="num">10
class="macro">#define  TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| System Resources                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\\Files\\Broker Manipulation\\EURUSD GBR D1.onnx" as const class="type">uchar onnx_proto[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global Variables                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long   model;
class="type">int    position_timer;
class="type">class="kw">double bid,ask;
class="type">class="kw">double o,h,l,c;
class="type">bool   bullish;

「EA 初始化里把 ONNX 模型接进行情流」

把训练好的 EURUSD 自编码器塞进 MT5,核心动作全在 OnInit 里完成:用 OnnxCreateFromBuffer 从内存缓冲区加载模型,再把输入维度锁成 {1, ONNX_INPUT_SHAPE}、输出维度锁成 {1, ONNX_OUTPUT_SHAPE}。任何一步拿不到合法 handle 或 shape 设置失败,直接 Comment 报错并返回 INIT_FAILED,EA 起不来。 初始化末尾顺手定了两个数:position_timer 归零,sl_width 写死 30——后者就是后续止损宽度的基准点数,做贵金属或外汇跨品种时按波动率改这个值就行。 OnDeinit 只干一件干净事:OnnxRelease(model) 释放模型句柄,避免反复加载拖内存。OnTick 则靠静态 time_stamp 做新 K 线过滤,只在 SYSTEM_TIME_FRAME 的 0 号 bar 时间变化时重抓 ask/bid 与 1、2 号 bar 的 OHLC,并算 bullish = (o<c)&&(c>前一根收盘),把价格行为信号喂给后面的模型推理。 外汇与贵金属杠杆高、滑点跳空频繁,sl_width=30 在欧盘平稳段可能够用,美盘数据行情里被扫概率明显上升,实盘前建议在策略测试器里按品种重标。

MQL5 / C++
class="type">class="kw">double sl_width;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   model = OnnxCreateFromBuffer(onnx_proto,ONNX_DATA_TYPE_FLOAT);
   class="type">ulong input_shape[] = {class="num">1,ONNX_INPUT_SHAPE};
   class="type">ulong output_shape[] = {class="num">1,ONNX_OUTPUT_SHAPE};
   if(model == INVALID_HANDLE)
     {
       Comment("Failed To Load EURUSD Auto-Encoder-Decoder: ",GetLastError());
       class="kw">return(INIT_FAILED);
     }
   if(!OnnxSetInputShape(model,class="num">0,input_shape))
     {
       Comment("Failed To Set EURUSD Auto-Encoder-Decoder Input Shape: ",GetLastError());
       class="kw">return(INIT_FAILED);
     }
   else
     if(!OnnxSetOutputShape(model,class="num">0,output_shape))
       {
        Comment("Failed To Set EURUSD Auto-Encoder-Decoder Output Shape: ",GetLastError());
        class="kw">return(INIT_FAILED);
       }
   position_timer = class="num">0;
   sl_width = class="num">30;
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">//---
   OnnxRelease(model);
  }
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(),SYSTEM_TIME_FRAME,class="num">0);
   if(time_stamp != current_time)
     {
       time_stamp = current_time;
       ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
       bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
       o = iOpen(Symbol(),SYSTEM_TIME_FRAME,class="num">1);
       h = iHigh(Symbol(),SYSTEM_TIME_FRAME,class="num">1);
       l = iLow(Symbol(),SYSTEM_TIME_FRAME,class="num">1);
       c = iClose(Symbol(),SYSTEM_TIME_FRAME,class="num">1);
       bullish = (o < c) && (c > iClose(Symbol(),SYSTEM_TIME_FRAME,class="num">2));

◍ 持仓状态下的止损与到期逻辑

这段控制流解决一个很实际的问题:没单时清零计时并找信号,有单时动态拉止损、到点强制平仓。外汇与贵金属波动剧烈,硬编码固定止损容易被扫,用波段幅度乘系数去追止损更贴合价格行为。 无持仓分支很简单——PositionsTotal() 为 0 就把 position_timer 归零并调用 find_setup() 扫描入场条件。有持仓后先 PositionSelect(Symbol()) 锁定当前品种订单,读出 POSITION_TYPE 与 POSITION_SL。 多头单的 new_sl 设在 bid - (h-l)*sl_width,仅当算出的止损比原止损更优(更大)才用 Trade.PositionModify 上移;空单镜像处理,new_sl = ask + (h-l)*sl_width,只在更小(更优)时下移。sl_width 是你可以直接调的旋钮,例如 0.5 代表取半个波段高宽作缓冲。 position_timer 每 tick 加 1,达到 RETURN_PERIOD 就 Trade.PositionClose 平掉。RETURN_PERIOD 若设 200,意味着持仓满 200 个计时单位后无论盈亏倾向离场,避免模型单漂成隔夜风险仓。 find_setup 里把当根 K 线的开高低塞进 vectorf model_inputs,维度按 ONNX_INPUT_SHAPE 定,说明信号来自轻量 ONNX 模型推理而非裸指标交叉。

MQL5 / C++
if(PositionsTotal() == class="num">0)
  {
   position_timer = class="num">0;
   find_setup();
  }
else
  if(PositionsTotal() > class="num">0)
    {
     if(PositionSelect(Symbol()))
       {
        class="type">long position_type = PositionGetInteger(POSITION_TYPE);
        class="type">class="kw">double current_sl = PositionGetDouble(POSITION_SL);
        class="type">class="kw">double new_sl;
        class=class="str">"cmt">//--- Buy Trades
        if(position_type == POSITION_TYPE_BUY)
          {
           new_sl = bid - ((h-l)*sl_width);
           if(new_sl > current_sl)
             Trade.PositionModify(Symbol(),new_sl,class="num">0);
          }
        class=class="str">"cmt">//--- Sell Trades
        else
          if(position_type == POSITION_TYPE_SELL)
            {
             new_sl = ask + ((h-l)*sl_width);
             if(new_sl < current_sl)
               Trade.PositionModify(Symbol(),new_sl,class="num">0);
            }
       }
     if(position_timer < RETURN_PERIOD)
        position_timer+=class="num">1;
     else
        Trade.PositionClose(Symbol());
    }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Find A Trading Setup                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void find_setup(class="type">void)
  {
   vectorf model_inputs(ONNX_INPUT_SHAPE);
   model_inputs[class="num">0] = (class="type">float) iOpen(Symbol(),SYSTEM_TIME_FRAME,class="num">0);
   model_inputs[class="num">1] = (class="type">float) iHigh(Symbol(),SYSTEM_TIME_FRAME,class="num">0);
   model_inputs[class="num">2] = (class="type">float) iLow(Symbol(),SYSTEM_TIME_FRAME,class="num">0);

把模型输出接进下单逻辑

这段代码片段承接前面的 ONNX 推理,把最新收盘价塞进模型输入的第 4 个槽位,然后跑一次前向预测。若 OnnxRun 返回失败,直接在图表上打印错误码并退出本次 tick,不碰仓位。 预测成功后会把 model_output[0] 显示出来,同时用 CopyRates 拉取当前周期最近两根 K 线的开盘与收盘,分别求均值。当开盘均值低于收盘均值(近两根偏向收阳),且模型输出大于 0 并处于多头标记状态时,才用 Trade.Buy 以 ask 入场,止损挂在 bid 减去波动带宽 sl_width 的位置。 反向情形里,开盘均值高于收盘均值且模型输出小于 0、空头标记成立,则 Trade.Sell 以 bid 入场,止损置于 ask 加波动带宽处。外汇与贵金属杠杆高,这类基于两K线均值的触发条件只是概率倾向,实盘前务必在 MT5 策略测试器用历史数据验证滑点与止损宽度表现。 末尾的 #undef 把 ONNX_INPUT_SHAPE、SYSTEM_TIME_FRAME 等宏全部撤掉,避免与其他 EA 模块的常量命名冲突。复制这段代码时注意 h、l、bullish、ask、bid 必须在前文已正确赋值,否则编译期就会报错。

MQL5 / C++
  model_inputs[class="num">3] = (class="type">float) iClose(Symbol(),SYSTEM_TIME_FRAME,class="num">0);
  vectorf model_output(ONNX_OUTPUT_SHAPE);
  if(!OnnxRun(model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_output))
    {
      Comment("Failed To Get A Prediction From Our Model: ",GetLastError());
      class="kw">return;
    }
  else
    {
      Comment("Prediction: ",model_output[class="num">0]);
      vector open,close;
      open.CopyRates(Symbol(),SYSTEM_TIME_FRAME,COPY_RATES_OPEN,class="num">1,class="num">2);
      close.CopyRates(Symbol(),SYSTEM_TIME_FRAME,COPY_RATES_CLOSE,class="num">1,class="num">2);
      if(open.Mean() < close.Mean())
        {
         if((model_output[class="num">0] > class="num">0) && (bullish))
            Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid - ((h-l) * sl_width)),class="num">0);
        }
      else
        if(open.Mean() > close.Mean())
          {
            if((model_output[class="num">0] < class="num">0) && (!bullish))
              Trade.Sell(TRADING_VOLUME,Symbol(),bid,(ask + ((h-l) * sl_width)),class="num">0);
          }
    }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Undefine System Constants                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef  ONNX_INPUT_SHAPE
class="macro">#undef  ONNX_OUTPUT_SHAPE
class="macro">#undef  SYSTEM_TIME_FRAME
class="macro">#undef  TRADING_VOLUME
class="macro">#undef  RETURN_PERIOD
class=class="str">"cmt">//+------------------------------------------------------------------+

「挑经纪商比挑模型更前置」

不同经纪商的报价分散性是个硬约束:同一套基于 ONNX 的 EA 在 A 券商能跑出的套利空间,换到 B 券商可能价格序列根本对不上,回测盈利直接变实盘亏损。我们附带的 EURUSD.mq5 就是用来在两家券商各跑一遍同模型、比盈利差的,6.94 KB 的源码可以直接丢进 MT5 回溯。 如果你常在社区发需求,别写「求购欧美交易程序」这种空话,直接指定券商并让开发者开同券商模拟账户,需求越细越不容易踩坑。买现成 EA 也优先选标明专属券商的,泛用版往往隐藏了点差和滑点错位。 信号订阅者把券商对齐这一步做了,供应商报告收益和你账户实际收益才可能对得上。外汇和贵金属本身高波动高杠杆,券商间数据不一致会再叠一层执行风险,验证前别轻易上实盘。 开发者这边要认一个账:跨券商共享单模型是把客户推到未知滑点里,交付前用 Requesting Broker Data.ipynb 拉两家日线、再用 Analyzing Broker Data.ipynb 做一致性检测,这两份 notebook 一个 2.69 KB 一个 595.1 KB,跑通再谈通用。

常见问题

大概率不是模型问题,而是不同经纪商报价延迟、点差、滑点差异导致特征漂移。先固定经纪商再调模型,比反过来更省事。
在 EA 初始化函数里加载 ONNX 句柄并绑定当前品种 tick 事件,每根 K 线或每次 tick 用模型推理输出特征,再喂给后续逻辑。
小布可基于历史报价质量、点差稳定性和成交滑点做横向诊断,直接标出哪些经纪商更适配你的模型,省去手动回测切换。
用独立定时器管到期,用模型信号触发移动止损或平仓;两者分通道写,避免信号频繁翻转把单子提前洗掉。
先看报价断层次数和极端点差时段,再用零策略基准(随机下单)测滑点分布,偏离中位数大的直接弃用。