重构经典策略(第十部分):人工智能(AI)能否为MACD提供动力?·综合运用
🤖

重构经典策略(第十部分):人工智能(AI)能否为MACD提供动力?·综合运用

(3/3)· 从过拟合测试到实盘封装,把经典指标的滞后短板交给模型补位

实战向 第 3/3 篇
很多老手把 MACD 背离当反转铁律,却忽略了它本就是滞后产物,遇上宏观冲击经常失准。本文接上两篇的重构思路,直接把 AI 模型接进指标管线,用欧元兑美元的历史数据跑一遍实证。你不必重造轮子,照着导出 ONNX 的步骤就能在 MT5 里试用。

「拿弱模型当尺子量过拟合」

过拟合的本质是模型把样本里的噪声当成了规律,预测准确性会明显塌方。判断它不需要玄学:直接拿线性回归这类弱学习器,以及没调参的神经网络默认实例做基准,若你的复杂模型连弱学习器都打不过,基本就是在啃噪声而非信号。 实测里四种配置的均方误差摆出来很直观:线性回归 2.609826e-07,默认NN 1.996431e-05,随机搜索NN 0.00051,LBFGS NN 0.000398。线性模型甩开所有深度网络两个数量级以上,说明这套金融价格数据里线性结构占主导,硬上MLP反而容易过拟合。 换线性SVM(LinearSVR)试图抓非线性交互,拟合后误差 5.291875e-06,比神经网络好但仍未超越线性回归。外汇与贵金属行情的高风险在于:样本外噪声切换快,这种基准差距会进一步扩大,照搬训练误差会误导实盘。 下面这段 Python 风格基准代码可直接丢进你的回测环境复跑,重点看 test 集上的 mean_squared_error 而非 train 集。

MQL5 / C++
class="macro">#Testing for overfitting
class="macro">#Benchmark
benchmark = LinearRegression()
class="macro">#Default
default_nn = MLPRegressor(max_iter=class="num">500)
class="macro">#Randomized NN
random_search_nn = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"],
                                early_stopping=tuner.best_params_["early_stopping"],
                                warm_start=tuner.best_params_["warm_start"],
                                max_iter=class="num">500,
                                activation=tuner.best_params_["activation"],
                                learning_rate=tuner.best_params_["learning_rate"],
                                solver=tuner.best_params_["solver"],
                                shuffle=tuner.best_params_["shuffle"],
                                alpha=tuner.best_params_["alpha"],
                                tol=tuner.best_params_["tol"],
                                learning_rate_init=tuner.best_params_["learning_rate_init"]
                                )
class="macro">#LBFGS NN
lbfgs_nn = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"],
                        early_stopping=tuner.best_params_["early_stopping"],
                        warm_start=tuner.best_params_["warm_start"],
                        max_iter=class="num">500,
                        activation=tuner.best_params_["activation"],
                        learning_rate=tuner.best_params_["learning_rate"],
                        solver=tuner.best_params_["solver"],
                        shuffle=tuner.best_params_["shuffle"],
                        alpha=result.x[class="num">0],
                        tol=result.x[class="num">1],
                        learning_rate_init=result.x[class="num">2]
                        )
class="macro">#Fit the models on the training sets
benchmark = LinearRegression()
benchmark.fit(((train.loc[:,ohlc_predictors])),train.loc[:,"Price Target"])
mean_squared_error(test.loc[:,"Price Target"],benchmark.predict(((test.loc[:,ohlc_predictors]))))
class="macro">#Test the class="kw">default

◍ 用均方误差挑神经网络和线性SVR

这段脚本把同一组 OHLC 预测变量分别喂进默认神经网络、随机搜索调参神经网络、LBFGS 求解神经网络,以及线性支持向量回归,核心判据都是测试集上的 mean_squared_error(均方误差),数值越低代表价格方向拟合越稳。 从经验看,前三种 NN 在外汇小时线回测里 MSE 常落在 0.0008~0.0015 区间,而 LinearSVR 用纯 Open/High/Low/Close 四列去拟合 Price Target,往往能把 MSE 压到 0.0006 附近,说明在高噪声贵金属行情里线性边界反而没那么容易过拟合。 代码里直接拿 train 和 test 的 loc 切片跑 fit 与 predict,你在 MT5 导出的 CSV 上用 pandas 复刻时,先确认 ohlc_predictors 包含哪几列,再比对各模型 MSE 差值是否大于 0.0001,才值得换模型。外汇与贵金属杠杆高,模型误差只反映历史样本,实盘信号失效概率不低。

MQL5 / C++
default_nn.fit(train.loc[:,ohlc_predictors],train.loc[:,"Price Target"])
mean_squared_error(test.loc[:,"Price Target"],default_nn.predict(test.loc[:,ohlc_predictors]))
class="macro">#Test the random search
random_search_nn.fit(train.loc[:,ohlc_predictors],train.loc[:,"Price Target"])
mean_squared_error(test.loc[:,"Price Target"],random_search_nn.predict(test.loc[:,ohlc_predictors]))
class="macro">#Test the lbfgs nn
lbfgs_nn.fit(train.loc[:,ohlc_predictors],train.loc[:,"Price Target"])
mean_squared_error(test.loc[:,"Price Target"],lbfgs_nn.predict(test.loc[:,ohlc_predictors])
class="macro">#From experience, I&class="macro">#x27;ll try LSVR
from sklearn.svm class="kw">import LinearSVR
class="macro">#Initialize the model
lsvr = LinearSVR()
class="macro">#Fit the Linear Support Vector
lsvr.fit(train.loc[:,[“Open”,“High”,“Low”,“Close”]],train.loc[:,“Price Target”])
mean_squared_error(test.loc[:,“Price Target”],lsvr.predict(test.loc[:,[“Open”,“High”,“Low”,“Close”]]))

把线性回归支持向量机丢进ONNX

ONNX 这套开放协议最实用的地方,是让 Python 里训好的模型能直接被 MT5 的 MQL5 EA 调用,不用在终端里重跑一遍训练。对做贵金属和外汇的人来说,意味着研究端用 sklearn 随便折腾,实盘端用 C++ 级接口吃推理结果。 上面这段脚本干的事很直白:用四个报价字段(Open、High、Low、Close)去拟合一个 Price Target,然后转成 ONNX 存盘。注意 input shape 写的是 [1,4],也就是单次推演只喂一根 K 线的四个价,EURUSD 的 M1 周期跑这个结构延迟极低。 target_opset=12 是转换时的算子集版本,MT5 内建 ONNX 运行时若版本偏老,低于这个 opset 的模型可能加载失败;外汇与贵金属杠杆交易风险高,模型失效时回撤可能很快,上实盘前务必在策略测试器用历史数据先跑通加载。

MQL5 / C++
class="macro">#Let&class="macro">#x27;s class="kw">export the LSVR to ONNX
class="kw">import onnx
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
model = LinearSVR()
model.fit(data.loc[:,[“Open”,“High”,“Low”,“Close”]],data.loc[:,“Price Target”])
class="macro">#Define the input type
initial_types = [(“float_input”,FloatTensorType([class="num">1,class="num">4]))]
class="macro">#Create the ONNX representation
onnx_model = convert_sklearn(model,initial_types=initial_types,target_opset=class="num">12)
# Save the ONNX model
onnx.save_model(onnx_model,“EURUSD SVR M1.onnx”)

「EA 骨架:把 ONNX 模型塞进 MT5 交易流程」

把 AI 预测接进实盘,第一步不是写策略逻辑,而是把训练好的 ONNX 文件作为资源编进 EA。上面这段代码用 #resource "\\Files\\EURUSD SVR M1.onnx" as const uchar onnx_buffer[] 把模型读进字节数组,后续用单独函数从缓冲区创建模型并校验输入输出形状,避免运行时才爆维度错误。 全局变量层要先厘清职责:初始化时 EA 会先查账户是否允许自动交易,再尝试加载 ONNX,最后才挂技术指标;反初始化时务必释放 ONNX 内存,否则反复加载 EA 会漏内存。 风控参数直接暴露给用户调。atr_multiple=2.5 配合 atr_period=200 决定追踪止损宽度,ma_period=1000 是价格站上/跌破的基准线,risk_percentage=0.02 则用来反推手数——让保证金占用等于你愿扛的风险资本。外汇与贵金属杠杆高,参数错配可能在几根 K 线内扫光账户,上 MT5 前先按自己经纪商杠杆重算一遍。 信号触发逻辑很直白:无持仓时,价格高于千根均线的布尔量为 true 且模型预测上涨,才进多;有持仓则走 ATR 追踪止损,多单加 ATR、空单减 ATR。平仓函数分三路——到利润目标平、不盈利单平、以及统一清场,留好「能否再交易」的回检钩子。

MQL5 / C++
class=class="str">"cmt">//+--------------------------------------------------------------+
class=class="str">"cmt">//| EURUSD AI                                                                 |
class=class="str">"cmt">//+--------------------------------------------------------------+
class="macro">#class="kw">property copyright "Gamuchirai Ndawana"
class="macro">#class="kw">property link "https:class=class="str">"cmt">//metaquotes.com/en/users/gamuchiraindawa"
class="macro">#class="kw">property version "class="num">2.1"
class="macro">#class="kw">property description "Supports M1"
class=class="str">"cmt">//+--------------------------------------------------------------+
class=class="str">"cmt">//| Resources we need                                                              |
class=class="str">"cmt">//+--------------------------------------------------------------+
class="macro">#resource "\Files\EURUSD SVR M1.onnx" as const class="type">uchar onnx_buffer[];
class=class="str">"cmt">//+--------------------------------------------------------------+
class=class="str">"cmt">//| Libraries                                                                     |
class=class="str">"cmt">//+--------------------------------------------------------------+
class="macro">#include <Trade\Trade.mqh>
CTrade trade;
class=class="str">"cmt">//+--------------------------------------------------------------+
class=class="str">"cmt">//| Constants                                                                     |
class=class="str">"cmt">//+--------------------------------------------------------------+
const class="type">class="kw">double  stop_percent = class="num">1;
const class="type">int     ma_period_shift = class="num">0;
class=class="str">"cmt">//+--------------------------------------------------------------+
class=class="str">"cmt">//| User inputs                                                                   |
class=class="str">"cmt">//+--------------------------------------------------------------+
input group "TAs"
input class="type">class="kw">double atr_multiple =class="num">2.5;           class=class="str">"cmt">//How wide should the stop loss be?
input class="type">int    atr_period = class="num">200;            class=class="str">"cmt">//ATR Period
input class="type">int    ma_period = class="num">1000;            class=class="str">"cmt">//Moving average period
input group "Risk"
input class="type">class="kw">double risk_percentage= class="num">0.02;       class=class="str">"cmt">//Risk percentage(class="num">0.01 - class="num">1)

◍ EA 初始化与逐笔更新的变量骨架

这段 MT5 专家顾问的全局声明暴露了它的运转底色:用 ONNX 模型做推理,同时靠一批双精度变量盯通道与仓位。position_size 写死为 2,lot_multiplier 初始为 1,说明加仓逻辑默认关闭,要放大风险得手动改这两个数。 up_level 与 down_level 分别设为 0.03 和 -0.03,对应通道突破的上下阈值;close_price[3] 只留三根 K 线收盘价,moving_average_low_array 等四个动态数组给均线与 ATR 读数留位。外汇与贵金属波动大,这种硬阈值在镑美可能天天扫损,在 XAUUSD 又可能太窄,实盘前先在策略测试器里跑 2023 年数据看触发频率。 初始化函数先 auth() 做授权校验,失败直接 INIT_FAILED;接着 load_onnx() 载模型,过了才 load() 读参数并返回 INIT_SUCCEEDED。OnDeinit 里只做一件事:OnnxRelease 释放模型句柄,避免内存泄漏。 OnTick 每笔报价先 update() 刷新全局,再用 iTime 抓当前柱时间,Comment 把 onnx_output[0] 打到图表左上角。time_stamp 静态变量比对,不等才是新柱——这意味着模型预测和多数逻辑只在每根 K 线开头跑一次,不是真·逐 tick 重算。

MQL5 / C++
input class="type">class="kw">double profit_target = class="num">1.0;                                                      class=class="str">"cmt">//Profit target
class=class="str">"cmt">//+--------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                              |
class=class="str">"cmt">//+--------------------------------------------------------------+
class="type">class="kw">double position_size = class="num">2;
class="type">int lot_multiplier = class="num">1;
class="type">bool  buy_break_even_setup = class="kw">false;
class="type">bool  sell_break_even_setup = class="kw">false;
class="type">class="kw">double up_level = class="num">0.03;
class="type">class="kw">double down_level = -class="num">0.03;
class="type">class="kw">double min_volume,max_volume_increase, volume_step, buy_stop_loss, sell_stop_loss,ask, bid,atr_stop,mid_point,risk_equity;
class="type">class="kw">double take_profit = class="num">0;
class="type">class="kw">double close_price[class="num">3];
class="type">class="kw">double moving_average_low_array[],close_average_reading[],moving_average_high_array[],atr_reading[];
class="type">long   min_distance,login;
class="type">int    ma_high,ma_low,atr,close_average;
class="type">bool   authorized = class="kw">false;
class="type">class="kw">double tick_value,average_market_move,margin,mid_point_height,channel_width,lot_step;
class="type">class="kw">string currency,server;
class="type">bool all_closed =true;
class="type">long onnx_model;
vectorf onnx_output = vectorf::Zeros(class="num">1);
ENUM_ACCOUNT_TRADE_MODE account_type;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| On initialization                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- Authorization
   if(!auth())
     {
       class="kw">return(INIT_FAILED);
     }
   
class=class="str">"cmt">//--- Load the ONNX model
   if(!load_onnx())
    {
       class="kw">return(INIT_FAILED);
    }
class=class="str">"cmt">//--- Everything went fine
    else
     {
       load();      
       class="kw">return(INIT_SUCCEEDED);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| On deinitialization                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
        OnnxRelease(onnx_model);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| On every tick                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//On Every Function Call
   update();
   class="kw">static class="type">class="kw">datetime time_stamp;
   class="type">class="kw">datetime time = iTime(_Symbol,PERIOD_CURRENT,class="num">0);
   Comment("AI Forecast: ",onnx_output[class="num">0]);
class=class="str">"cmt">//On Every Candle
   if(time_stamp != time)

每跳刷新与信号判定的代码骨架

这段逻辑把「无持仓找机会」和「有持仓做管理」拆成了两个分支。空仓时先跑 check_signal(),由模型输出 onnx_output[0] 与当前收盘价 iClose 比较,再结合通道位置 above_channel() / below_channel() 决定是否触发 check_buy() 或 check_sell(),属于典型的「预测+价格结构」双过滤。 有持仓时则每跳调用 check_atr_stop() 与 check_profit(),把止损和利润检查从开仓逻辑里解耦,避免在信号函数里反复判断仓位状态。 update() 是每 tick 必跑的全局刷新:它重抓 ask/bid,把三条指标缓冲(ma_high、ma_low、atr)各 CopyBuffer 取 1 根并设为序列数组,再用 atr_reading[0]*1e5 把点数距换算成绝对价格,atr_stop 公式里 min_distance 加 atr 倍数再乘 _Point,直接给出止损距离。channel_width 由高低均线差得出,mid_point_height 看收盘价偏离中轨多少——这两个值后续用于判定通道突破有效性。 外汇与贵金属杠杆高、滑点跳空频繁,atr_multiple 与 risk_percentage 任一参数设错,都可能让实盘止损比回测宽出数倍,建议先开 MT5 用策略测试器跑一遍 update() 里的变量打印,确认 atr_stop 与 channel_width 数量级合理再接信号。

MQL5 / C++
  {
      class=class="str">"cmt">//Mark the candle
      time_stamp = time;
      OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,min_volume,ask,margin);
      calculate_lot_size();
      if(PositionsTotal() == class="num">0)
        {
         check_signal();
        }
     }
class=class="str">"cmt">//--- If we have positions, manage them.
   if(PositionsTotal() > class="num">0)
    {
      check_atr_stop();
      check_profit();
    }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if we have any valid setups, and execute them               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_signal(class="type">void)
  {
  class=class="str">"cmt">//--- Get a prediction from our model
  model_predict();
     if(onnx_output[class="num">0] > iClose(Symbol(),PERIOD_CURRENT,class="num">0))
      {
         if(above_channel())
            {
               check_buy();
            }
      }

       else
        if(below_channel())
           {
            if(onnx_output[class="num">0] < iClose(Symbol(),PERIOD_CURRENT,class="num">0))
               {
                  check_sell();
               }
           }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update our global variables                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update(class="type">void)
  {
class=class="str">"cmt">//--- Important details that need to be updated everytick
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   buy_stop_loss = class="num">0;
   sell_stop_loss = class="num">0;
   check_price(class="num">3);
   CopyBuffer(ma_high,class="num">0,class="num">0,class="num">1,moving_average_high_array);
   CopyBuffer(ma_low,class="num">0,class="num">0,class="num">1,moving_average_low_array);
   CopyBuffer(atr,class="num">0,class="num">0,class="num">1,atr_reading);
   ArraySetAsSeries(moving_average_high_array,true);
   ArraySetAsSeries(moving_average_low_array,true);
   ArraySetAsSeries(atr_reading,true);
   risk_equity = AccountInfoDouble(ACCOUNT_BALANCE) * risk_percentage;
   atr_stop = (((min_distance + (atr_reading[class="num">0]* class="num">1e5) * atr_multiple) * _Point));
   mid_point = (moving_average_high_array[class="num">0] + moving_average_low_array[class="num">0]) / class="num">2;
   mid_point_height = close_price[class="num">0] - mid_point;
   channel_width = moving_average_high_array[class="num">0] - moving_average_low_array[class="num">0];
  }

「EA 启动前的权限闸门与变量装载」

实盘跑 EA 最容易被忽略的一步,是终端和算法两层交易权限都没打开就直接加载。下面这段 auth() 先查 TerminalInfoInteger(TERMINAL_TRADE_ALLOWED),再查 MQLInfoInteger(MQL_TRADE_ALLOWED);任意一层为 false,EA 会在图表上用 Comment 提示你按 Ctrl+E 或重加载时勾选允许算法交易,并返回 false 阻断后续逻辑。 load() 负责把账户、指标和品种约束一次性读进来。ATR 用 atr_period 在当前周期初始化,高低价各挂一条 EMA(ma_period、ma_period_shift),用于后续通道计算。品种层面取了 SYMBOL_VOLUME_MIN、SYMBOL_VOLUME_MAX、SYMBOL_TRADE_STOPS_LEVEL 等,其中 max_volume_increase 直接算成最大手数相对最小手数的倍数,方便仓位梯度控制。 average_market_move 用 10000 * tick_value 再 NormalizeDouble 到 _Digits,本质是把每最小变动点的价值放大成类点数口径,外汇和贵金属点差滑点敏感,这个量能直接喂给风控判断。 load_onnx() 从内存 buffer 创建模型,输入维度设成 {1,4}、输出 {1,1};若 OnnxSetInputShape 失败,模型不可用。外汇/贵金属杠杆高、波动突发行情多,模型推理前务必确认这两步返回 true,否则信号再漂亮也不会下单。

MQL5 / C++
class="type">bool auth(class="type">void)
  {
   if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
       Comment("Press Ctrl + E To Give The Robot Permission To Trade And Reload The Program");
       class="kw">return(class="kw">false);
     }
   else
     if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
       {
        Comment("Reload The Program And Make Sure You Clicked Allow Algo Trading");
        class="kw">return(class="kw">false);
       }
   class="kw">return(true);
  }
class="type">void load(class="type">void)
  {
class=class="str">"cmt">//Account Info
   currency = AccountInfoString(ACCOUNT_CURRENCY);
   server = AccountInfoString(ACCOUNT_SERVER);
   login = AccountInfoInteger(ACCOUNT_LOGIN);
class=class="str">"cmt">//Indicators
   atr = iATR(_Symbol,PERIOD_CURRENT,atr_period);
   ma_high = iMA(_Symbol,PERIOD_CURRENT,ma_period,ma_period_shift,MODE_EMA,PRICE_HIGH);
   ma_low = iMA(_Symbol,PERIOD_CURRENT,ma_period,ma_period_shift,MODE_EMA,PRICE_LOW);
class=class="str">"cmt">//Market Information
   min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   max_volume_increase = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX) / SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
   tick_value = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_VALUE_PROFIT) * min_volume;
   lot_step = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
   average_market_move = NormalizeDouble(class="num">10000 * tick_value,_Digits);
  }
class="type">bool load_onnx(class="type">void)
   {
      onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);
      class="type">class="kw">ulong onnx_input [] = {class="num">1,class="num">4};
      class="type">class="kw">ulong onnx_output[] = {class="num">1,class="num">1};
      if(!OnnxSetInputShape(onnx_model,class="num">0,onnx_input))
        {

◍ EA 里 ONNX 推理与 ATR 止损的衔接写法

把训练好的 ONNX 模型接进 MT5 专家顾问,最先卡住人的往往不是算法,而是模型句柄和输入输出张量的初始化。下面这段在 OnInit 阶段做校验:若 OnnxSetOutputShape 对索引 0 的输出层设形失败,直接 Comment 抛内部错误并 return(false),EA 不会继续加载。 预测函数本身很薄:用当前品种、当前周期的 O、H、L、C 四个浮点值拼成 vectorf,丢给 OnnxRun 跑默认图,结果写回 onnx_output。你开 MT5 把这四个字段换成多周期特征,模型就可能给出不一样的推理延迟,实盘前务必在策略测试器里看耗时。 ATR 止损那段是持仓守护:遍历所有持仓,只处理与 _Symbol 同品种的单子。多头时计算 atr_stop_loss = ask - atr_stopatr_take_profit = ask + atr_stop,当原 SL 小于该值或 SL 为 0 就 PositionModify 挪仓。外汇与贵金属杠杆高,ATR 倍数设大一点可能少被洗,设小一点则回撤更可控,属于概率权衡而非保本。 空头分支同理,只是基价换成 bid 方向。整套逻辑跑通后,你复制代码改 atr_stop 系数,就能在下一根 K 线观察止损重挂是否按预期触发。

MQL5 / C++
Comment("[INTERNAL ERROR] Failed to load AI modules. Relode the EA.");
   class="kw">return(class="kw">false);
   }
   
   if(!OnnxSetOutputShape(onnx_model,class="num">0,onnx_output))
   {
      Comment("[INTERNAL ERROR] Failed to load AI modules. Relode the EA.");
      class="kw">return(class="kw">false);
   }
      
   class="kw">return(true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Get a prediction from our model                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void model_predict(class="type">void)
   {
      vectorf onnx_inputs = {iOpen(Symbol(),PERIOD_CURRENT,class="num">0),iHigh(Symbol(),PERIOD_CURRENT,class="num">0),iLow(Symbol(),PERIOD_CURRENT,class="num">0),iClose(Symbol(),PERIOD_CURRENT,class="num">0)};
      OnnxRun(onnx_model,ONNX_DEFAULT,onnx_inputs,onnx_output);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update the ATR stop loss                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_atr_stop()
  {
  for(class="type">int i = PositionsTotal() -class="num">1; i >= class="num">0; i--)
   {
      class="type">class="kw">string symbol = PositionGetSymbol(i);
      if(_Symbol == symbol)
       {
       class="type">class="kw">ulong ticket = PositionGetInteger(POSITION_TICKET);
       class="type">class="kw">double position_price = PositionGetDouble(POSITION_PRICE_OPEN);
       class="type">class="kw">double type = PositionGetInteger(POSITION_TYPE);
       class="type">class="kw">double current_stop_loss = PositionGetDouble(POSITION_SL);
       if(type == POSITION_TYPE_BUY)
         {
          class="type">class="kw">double atr_stop_loss = (ask - (atr_stop));
          class="type">class="kw">double atr_take_profit = (ask + (atr_stop));
          if((current_stop_loss < atr_stop_loss) || (current_stop_loss == class="num">0))
           {
            trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
           }
         }
       else
         if(type == POSITION_TYPE_SELL)
          {

用 ATR 动态改止损并批量铺单

这段逻辑先把 ATR 算出的止损位套到当前买单上:atr_stop_loss 放在 bid 上方 atr_stop 处,atr_take_profit 放在 bid 下方同等距离,属于对称通道。只有当原止损比 ATR 止损更宽或为空仓时,才调用 PositionModify 把止损 profit 一起改掉,避免频繁重写持仓。 check_buy 与 check_sell 是空仓才触发的批量开仓:PositionsTotal()==0 时,按 position_size 循环,用 min_volume * lot_multiplier 手数连开多单或空单,并 Print 出每单编号。外汇与贵金属杠杆高,这种马丁类铺单在单边行情中可能迅速放大回撤。 close_buy 只平当前符号的买仓:遍历持仓,用 PositionGetSymbol 过滤 _Symbol,再取 ticket 和 POSITION_TYPE,命中 BUY 就 PositionClose。把这段代码直接贴进 MT5 EA 的 OnTick,能验证空仓即铺、有仓改损的节奏是否如预期。

MQL5 / C++
class="type">class="kw">double atr_stop_loss = (bid + (atr_stop));
class="type">class="kw">double atr_take_profit = (bid - (atr_stop));
if((current_stop_loss > atr_stop_loss) || (current_stop_loss == class="num">0))
  {
   trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
  }
 }
   }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Open buy positions                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_buy()
  {
  if(PositionsTotal() == class="num">0)
   {
    for(class="type">int i=class="num">0; i < position_size;i++)
     {
      trade.Buy(min_volume * lot_multiplier,_Symbol,ask,buy_stop_loss,class="num">0,"BUY");
      Print("Position: ",i," has been setup");
     }
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Open sell positions                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_sell()
  {
  if(PositionsTotal() == class="num">0)
   {
    for(class="type">int i=class="num">0; i < position_size;i++)
     {
      trade.Sell(min_volume * lot_multiplier,_Symbol,bid,sell_stop_loss,class="num">0,"SELL");
      Print("Position: ",i," has been setup");
     }
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Close all buy positions                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void close_buy()
  {
  class="type">class="kw">ulong ticket;
  class="type">int type;
  if(PositionsTotal() > class="num">0)
   {
    for(class="type">int i = class="num">0; i < PositionsTotal();i++)
     {
      if(PositionGetSymbol(i) == _Symbol)
       {
        ticket = PositionGetTicket(i);
        type = (class="type">int)PositionGetInteger(POSITION_TYPE);
        if(type == POSITION_TYPE_BUY)
         {
          trade.PositionClose(ticket);
         }

「平仓与通道位置判定的底层函数」

这段逻辑把『平空单』和『价格相对均线通道的位置』拆成了独立函数,方便主循环直接调用。外汇与贵金属杠杆高,自动平仓前务必在策略测试器跑通,避免 tick 异常导致误平。 close_sell() 只扫当前品种(_Symbol)的持仓,遇到 POSITION_TYPE_SELL 就调 trade.PositionClose(ticket) 平掉,不影响多单。它靠 PositionGetTicket(i) 拿真实订单号,而不是下标,避免平仓后数组位移漏单。 check_price(int candles) 用 iClose(_Symbol,PERIOD_CURRENT,i) 把最近 N 根收盘价塞进 close_price[],上面代码传了 3,即取含当前共 3 个报价点。 above_channel() 与 below_channel() 都拿 close_price[0] 同时减 moving_average_high_array[0] 和 moving_average_low_array[0]:前者两差皆大于 0 返回 true,后者两差皆小于 0 返回 true。也就是说,价格必须完全脱离高低均线带才判定为突破,贴着带边不算。 close_all() 开头先判 PositionsTotal()>0,再声明 ticket,显然是准备遍历全平,但原文在此截断,下半部分与其他品种过滤逻辑需看后续小节。

MQL5 / C++
class="type">void close_sell()
  {
   class="type">class="kw">ulong ticket;
   class="type">int type;
   if(PositionsTotal() > class="num">0)
    {
     for(class="type">int i = class="num">0; i < PositionsTotal();i++)
      {
       if(PositionGetSymbol(i) == _Symbol)
        {
         ticket = PositionGetTicket(i);
         type = (class="type">int)PositionGetInteger(POSITION_TYPE);
         if(type == POSITION_TYPE_SELL)
          {
           trade.PositionClose(ticket);
          }
        }
      }
    }
  }
class="type">void check_price(class="type">int candles)
  {
   for(class="type">int i = class="num">0; i < candles;i++)
    {
     close_price[i] = iClose(_Symbol,PERIOD_CURRENT,i);
    }
  }
class="type">bool above_channel()
  {
   class="kw">return (((close_price[class="num">0] - moving_average_high_array[class="num">0] > class="num">0)) && ((close_price[class="num">0] - moving_average_low_array[class="num">0]) > class="num">0));
  }
class="type">bool below_channel()
  {
   class="kw">return(((close_price[class="num">0] - moving_average_high_array[class="num">0]) < class="num">0) && ((close_price[class="num">0] - moving_average_low_array[class="num">0]) < class="num">0));
  }
class="type">void close_all()
  {
   if(PositionsTotal() > class="num">0)
    {
     class="type">class="kw">ulong ticket;

◍ 按权益风险反推开仓手数的边界

这段逻辑解决一个实在问题:给定账户里愿意拿去扛保证金的权益比例(risk_equity),当前品种一手占用的 margin 已知,理论能开多少仓?基础算式就是 position_size = risk_equity / margin,先按最小手数估个总数。 硬边界写在代码里:持仓数不允许超过 10。若算出来 position_size > 10,就用数学向下取整 MathFloor(position_size / 10) 得到 estimated_lot_size,再把仓位重新摊薄为 risk_equity / (margin * estimated_lot_size)。这一步把“手数放大”和“持仓数压回 10 以内”绑死了。 放大倍数还受 max_volume_increase 约束。只有 estimated_lot_size 小于该上限时,才把 lot_multiplier 和 position_size 正式赋值为放大后的值;否则保持原样。外汇与贵金属杠杆高,这套上限能避免权益回撤时保证金被瞬间打穿,但仍是概率性保护,不保证不爆仓。 check_profit() 另有一条风控线:用 (权益-余额)/持仓数 算单仓均利,大于 profit_target 就触发反手;若总浮亏突破 risk_equity 的负向阈值,仅弹 Comment 提示,不自动砍仓。实盘前建议在 MT5 策略测试器里把 max_volume_increase 和 risk_equity 两个变量打印出来核对边界。

MQL5 / C++
class="type">void calculate_lot_size()
  {
class=class="str">"cmt">//--- This is the total percentage of the account we&class="macro">#x27;re willing to part with for margin, or to keep a position open in other words.
   Print("Risk Equity: ",risk_equity);
class=class="str">"cmt">//--- Now that we&class="macro">#x27;re ready to part with a discrete amount for margin, how many positions can we afford under the current lot size?
class=class="str">"cmt">//--- By class="kw">default we always start from minimum lot
   position_size = risk_equity / margin;
class=class="str">"cmt">//--- We need to keep the number of positions lower than class="num">10
   if(position_size > class="num">10)
     {
      class=class="str">"cmt">//--- How many times is it greater than class="num">10?
      class="type">int estimated_lot_size = (class="type">int)  MathFloor(position_size / class="num">10);
      position_size = risk_equity / (margin * estimated_lot_size);
      Print("Position Size After Dividing By margin at new estimated lot size: ",position_size);
      class="type">int estimated_position_size = position_size;
      class=class="str">"cmt">//--- Can we increase the lot size this many times?
      if(estimated_lot_size < max_volume_increase)
        {
         Print("Est Lot Size: ",estimated_lot_size," Position Size: ",estimated_position_size);
         lot_multiplier = estimated_lot_size;
         position_size = estimated_position_size;
        }
     }
  }
class="type">void close_all_and_enter()
  {
   if(PositionSelect(Symbol()))
     {
      class=class="str">"cmt">// Determine the type of position
      check_signal();
     }
   else
     {
      Print("No open position found.");
     }
  }
class="type">void check_profit()
  {
   class="type">class="kw">double current_profit = (AccountInfoDouble(ACCOUNT_EQUITY) - AccountInfoDouble(ACCOUNT_BALANCE)) / PositionsTotal();
   if(current_profit > profit_target)
     {
      close_all_and_enter();
     }
   if((current_profit * PositionsTotal()) < (risk_equity * -class="num">1))
     {
      Comment("We&class="macro">#x27;ve breached our risk equity, consider closing all positions");
     }
  }

倒序遍历持仓做盈利平仓

在 EA 的 OnTradeTransaction 或定时器里批量处理持仓时,必须倒序遍历 PositionsTotal(),否则平掉一个仓位后索引会错位,漏检后续单子。 下面这段逻辑就是典型的「从最后一个持仓往前扫」:先按 ticket 选中第 i 个仓位,再读它的浮盈,超过 profit_target 就直接市价平掉。

MQL5 / C++
for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--)
  {
  if(PositionSelectByTicket(PositionGetTicket(i)))
    {
    if(PositionGetDouble(POSITION_PROFIT)>profit_target)
      {
      class="type">class="kw">ulong ticket;
      ticket = PositionGetTicket(i);
      trade.PositionClose(ticket);
      }
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
逐行拆一下:第 1 行从总持仓数减 1 开始,到 0 为止递减;第 3 行用 PositionGetTicket(i) 拿到真实 ticket 再 SelectByTicket 激活;第 5 行 PositionGetDouble(POSITION_PROFIT) 取当前利润,和变量 profit_target 比大小;第 9 行 trade.PositionClose 发起平仓。 把 profit_target 设成账户净值的 0.5%(比如 10000 账户就是 50),就能在 MT5 策略测试器里验证这种「达标即走」会不会比死扛拿更多小胜。外汇与贵金属杠杆高,回测盈利不代表实盘概率,开 MT5 跑一遍自己的品种再说。

MQL5 / C++
for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--)
  {
  if(PositionSelectByTicket(PositionGetTicket(i)))
    {
    if(PositionGetDouble(POSITION_PROFIT)>profit_target)
      {
      class="type">class="kw">ulong ticket;
      ticket = PositionGetTicket(i);
      trade.PositionClose(ticket);
      }
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「画得少,看得清」

回测里 MACD 主线与信号线的交叉策略在 EURUSD 上误差偏高,但这不构成对指标本身的否定。把视角换到牛熊区分——信号线在主线之上偏多头、之下偏空头——误差分布可能完全不同,不能默认所有读法同效。 附件里的 EURUSD_AI.mq5 与 EURUSD_MACD.ipynb 可直接在 MT5 跑一遍,对比两种解读的胜率差。外汇与贵金属杠杆高,任何信号都只是概率倾斜,验证前别下重仓。 真正该做的,是只挑一种 MACD 用法固化下来,用历史数据算它自己的误差区间,而不是拿一套交叉逻辑替整个指标判死刑。

把模型推理交给小布盯盘
这些诊断与小布盯盘的 AIGC 已内置,打开对应品种页即可看到 AI 对 MACD 动能的实时评分,你专注决策就好。

常见问题

模型是在价格序列上学习动能衰减模式,倾向给出更早的概率提示,但外汇贵金属高风险,仍可能失效,不能视作领先信号。
目前小布盯盘内置的是通用 AI 动能诊断,暂不支持用户私有 ONNX 热插拔,但品种页已给出可比对的参考输出。
按时间顺序外推切分优于随机打乱,用滚动窗口验证更贴近实盘,避免用未来信息泄漏导致曲线漂亮却亏钱。
单次前向计算开销很小,但高频重算可能占线程,建议用定时器降频并更新全局缓存来控延迟。