在 MQL5 中构建自优化EA(第六部分):自适应交易规则(二)·综合运用
🤖

在 MQL5 中构建自优化EA(第六部分):自适应交易规则(二)·综合运用

(3/3)·固定周期和70/30老规则早不够用,这篇把自动挑周期、量化水平值的闭环一次讲透

含代码示例 第 3/3 篇
不少量化交易者把 RSI 周期写死在 EA 里,遇到横盘或单边就频繁假信号或完全沉默。以为调一下水平值就能解决,其实连周期本身都可能是错的。本篇把周期选择与水平量化一起交给算法。

把多周期RSI差值落盘成CSV

做外汇或贵金属量化复盘时,单周期RSI往往不够看。这段代码把周期 5 到 70(步长 5)共 14 条 RSI 曲线,以及它们相对 HORIZON 根 K 线前的差分结果,一次性写进 CSV 文件,方便后续用小布盯盘做横向扫描。 先给每个 RSI 数组灌入指标值:SetIndicatorValues 存原始读数,SetDifferencedIndicatorValues 按 HORIZON 偏移算差分,两者都带 true 强制刷新。循环里从 size 倒序到 1,第一行写表头,之后每行写时间、真实收盘价,以及开高低收相对 HORIZON 前的价差。 表头里能看到 RSI 5 到 RSI 70 与对应的 Diff RSI 系列,说明数据维度覆盖短中长三类动量。贵金属和外汇杠杆高,差分RSI只是概率参考,实盘前务必在MT5策略测试器跑一遍样本外数据。

MQL5 / C++
   class=class="str">"cmt">//--- Set the RSI buffers
   my_rsi_array[i].SetIndicatorValues(fetch,true);
   my_rsi_array[i].SetDifferencedIndicatorValues(fetch,HORIZON,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","True Close","Open","High","Low","Close","RSI class="num">5","RSI class="num">10","RSI class="num">15","RSI class="num">20","RSI class="num">25","RSI class="num">30","RSI class="num">35","RSI class="num">40","RSI class="num">45","RSI class="num">50","RSI class="num">55","RSI class="num">60","RSI class="num">65","RSI class="num">70","Diff RSI class="num">5","Diff RSI class="num">10","Diff RSI class="num">15","Diff RSI class="num">20","Diff RSI class="num">25","Diff RSI class="num">30","Diff RSI class="num">35","Diff RSI class="num">40","Diff RSI class="num">45","Diff RSI class="num">50","Diff RSI class="num">55","Diff RSI class="num">60","Diff RSI class="num">65","Diff RSI class="num">70");
      }
      else
      {
       FileWrite(file_handle,
                 iTime(_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),
                 my_rsi_array[class="num">0].GetReadingAt(i),
                 my_rsi_array[class="num">1].GetReadingAt(i),
                 my_rsi_array[class="num">2].GetReadingAt(i),
                 my_rsi_array[class="num">3].GetReadingAt(i),
                 my_rsi_array[class="num">4].GetReadingAt(i),
                 my_rsi_array[class="num">5].GetReadingAt(i),
                 my_rsi_array[class="num">6].GetReadingAt(i),
                 my_rsi_array[class="num">7].GetReadingAt(i),

◍ 把 14 条 RSI 序列落盘后再释放

这段收尾逻辑紧接前面的循环:先把索引 8 到 13 的原始 RSI 读数、以及 0 到 13 的差分读数依次写入同一行,随后关闭文件句柄。 文件写出用的是 FileClose(file_handle),句柄一旦关闭,本次 CSV 便定型,后面再想追加得重新打开。 RSI 对象指针放在一个 0 到 13 的 for 循环里统一 delete,共 14 个实例,漏删任何一个都会在 EA 反复加载时堆出内存碎片。 开 MT5 把这段接在你自己的 HORIZON 宏后面跑一遍,重点看文件行数是否等于数据条数、以及日志里有没有 delete 报错——外汇与贵金属波动剧烈,这类资源泄漏在长周期回测里可能拖垮终端。

MQL5 / C++
   my_rsi_array[class="num">8].GetReadingAt(i),
   my_rsi_array[class="num">9].GetReadingAt(i),
   my_rsi_array[class="num">10].GetReadingAt(i),
   my_rsi_array[class="num">11].GetReadingAt(i),
   my_rsi_array[class="num">12].GetReadingAt(i),
   my_rsi_array[class="num">13].GetReadingAt(i),
   my_rsi_array[class="num">0].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">1].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">2].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">3].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">4].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">5].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">6].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">7].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">8].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">9].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">10].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">11].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">12].GetDifferencedReadingAt(i),
   my_rsi_array[class="num">13].GetDifferencedReadingAt(i)
   );
   }
  }
class=class="str">"cmt">//--- Close the file
  FileClose(file_handle);
class=class="str">"cmt">//--- Delete our RSI object pointers
  for(class="type">int i = class="num">0; i <= class="num">13; i++)
  {
    class="kw">delete my_rsi_array[i];
  }
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef HORIZON

「用岭回归给 14 个 RSI 周期称重」

把 MT5 导出的 EURUSD 日线丢进 Python 后,先按 10 天前收盘价打上 Bull 标记,再算出 10 日真实回报做目标列。回测期之外的数据必须切掉——代码里用 iloc 把最后约 3 年减去 95 天的窗口完全剔除,避免用未来信息骗自己。 手跑 14 次回测挑 RSI 周期太笨。直接把 14 个周期的 RSI 差值全喂给 Ridge 模型,用网格搜 Alpha 和 Tolerance,让系数自己收缩到接近 0。在 sparse_cg 求解器下,Alpha 设为 0 那组误差最低,仅 0.053509;Alpha 调到第 9 档时误差升到 0.085171。 表现最好的模型把最大绝对值系数给了 55 周期 RSI 的 10 周期变化。进一步以 10 为步长搜临界水平:以 0 为界误差 0.0269,用 30/70 反而涨到 0.0313,精细搜后真实最优落在 9 附近——但死抠历史最优是过拟合,取最优值两侧标准差当置信带更稳。 置信区间算出来是 [7.18, 10.82]。Ridge 解释向量 [1.97e-04, -1.64e-04, -7.55e-05] 表明:55 周期 RSI 变化超 9 时模型学成正系数倾向做多,其余条件倾向做空。外汇高杠杆下这仅是概率倾向,开 MT5 接 EA 前先拿样本外年份验一遍。

MQL5 / C++
class="kw">import pandas as pd
class="kw">import numpy as np
class="kw">import seaborn as sns
class="kw">import matplotlib.pyplot as plt
class="kw">import plotly
class="macro">#Let&class="macro">#x27;s read in our market data
data = pd.read_csv("EURUSD RSI Algorithmic Input Selection.csv")
data[&class="macro">#x27;Bull&class="macro">#x27;] = np.NaN
data.loc[data[&class="macro">#x27;True Close&class="macro">#x27;] > data[&class="macro">#x27;True Close&class="macro">#x27;].shift(class="num">10),&class="macro">#x27;Bull&class="macro">#x27;] = class="num">1
data.loc[data[&class="macro">#x27;True Close&class="macro">#x27;] < data[&class="macro">#x27;True Close&class="macro">#x27;].shift(class="num">10),&class="macro">#x27;Bull&class="macro">#x27;] = class="num">0
data.dropna(inplace=True)
data.reset_index(inplace=True,drop=True)
class="macro">#Estimate the market returns
class="macro">#Define our forecast horizon
HORIZON = class="num">10
data[&class="macro">#x27;Target&class="macro">#x27;] = class="num">0
data[&class="macro">#x27;Return&class="macro">#x27;] = data[&class="macro">#x27;True Close&class="macro">#x27;].shift(-HORIZON) - data[&class="macro">#x27;True Close&class="macro">#x27;]
data.loc[data[&class="macro">#x27;Return&class="macro">#x27;] > class="num">0,&class="macro">#x27;Target&class="macro">#x27;] = class="num">1
class="macro">#Drop missing values
data.dropna(inplace=True)
class="macro">#No cheating boys.
_ = data.iloc[((-class="num">365 * class="num">3) + class="num">95):,:]
data = data.iloc[:((-class="num">365 * class="num">3) + class="num">95),:]
data
plt.title(&class="macro">#x27;Distribution of EURUSD class="num">10 Day Returns&class="macro">#x27;)
plt.grid()
sns.histplot(data[&class="macro">#x27;Return&class="macro">#x27;],class="type">color=&class="macro">#x27;black&class="macro">#x27;)
plt.title(&class="macro">#x27;Comapring The Distribution of RSI Changes Across Different RSI Periods&class="macro">#x27;)
sns.histplot(data[&class="macro">#x27;RSI class="num">5&class="macro">#x27;],class="type">color=&class="macro">#x27;black&class="macro">#x27;)
sns.histplot(data[&class="macro">#x27;RSI class="num">70&class="macro">#x27;],class="type">color=&class="macro">#x27;white&class="macro">#x27;)
plt.xlabel(&class="macro">#x27;RSI Level&class="macro">#x27;)
plt.legend([&class="macro">#x27;RSI class="num">5&class="macro">#x27;,&class="macro">#x27;RSI class="num">70&class="macro">#x27;])
plt.axvline(class="num">30,class="type">color=&class="macro">#x27;red&class="macro">#x27;,linestyle=&class="macro">#x27;--&class="macro">#x27;)
plt.axvline(class="num">70,class="type">color=&class="macro">#x27;red&class="macro">#x27;,linestyle=&class="macro">#x27;--&class="macro">#x27;)
plt.grid()

用岭回归网格搜 EURUSD 的 RSI 差分预测误差

把 5 到 70 周期 RSI 的 10 日差分作为特征,去回归 EURUSD 的 10 日收益率,这一步先用散点图看 50 周期与 5 周期 RSI 差分和后续回报的分布,红色虚线标在 ±10 处,肉眼能看出极端 RSI 变动后价格倾向有反向或延续动作,但相关性并不强。 真正定量的是网格搜索:设 ALPHA_LEVELS 与 TOL_LEVELS 各为 10,对 Ridge 回归的 alpha 与 tol 取 10^(-j) 与 10^(-i) 组合,用时间序列交叉验证算平均绝对误差。特征矩阵覆盖 'Diff RSI 5' 到 'Diff RSI 70' 共 14 列,cv 用 TimeSeriesSplit,误差写进 10×10 的 DataFrame。 用 plotly 画 contour,z 轴是误差矩阵,蓝红配色下颜色越红代表误差越低。实盘前可在 MT5 导出自家 EURUSD 的 RSI 差分序列,跑同样网格看你的样本里最低误差落在 alpha、tol 的哪一格。 最后用 alpha=0、tol=0 的 Ridge 拟合全量数据,后续可提取 coef_ 看哪个 RSI 周期差分权重最大——外汇与贵金属杠杆高,这类线性模型只揭示统计倾向,不能直接当作入场信号。

MQL5 / C++
plt.title(&class="macro">#x27;Scatter Plot of class="num">10 Day Change in class="num">50 Period RSI & EURUSD class="num">10 Day Return&class="macro">#x27;)
sns.scatterplot(data,y=&class="macro">#x27;Diff RSI class="num">60&class="macro">#x27;,x=&class="macro">#x27;Diff RSI class="num">60&class="macro">#x27;,hue=&class="macro">#x27;Target&class="macro">#x27;)
plt.xlabel(&class="macro">#x27;class="num">50 Period RSI&class="macro">#x27;)
plt.ylabel(&class="macro">#x27;class="num">50 Period RSI&class="macro">#x27;)
plt.grid()
plt.axvline(-class="num">10,class="type">color=&class="macro">#x27;red&class="macro">#x27;,linestyle=&class="macro">#x27;--&class="macro">#x27;)
plt.axvline(class="num">10,class="type">color=&class="macro">#x27;red&class="macro">#x27;,linestyle=&class="macro">#x27;--&class="macro">#x27;)
plt.title(&class="macro">#x27;Scatter Plot of class="num">10 Day Change in class="num">5 Period RSI & EURUSD class="num">10 Day Return&class="macro">#x27;)
sns.scatterplot(data,y=&class="macro">#x27;Diff RSI class="num">60&class="macro">#x27;,x=&class="macro">#x27;Diff RSI class="num">5&class="macro">#x27;,hue=&class="macro">#x27;Target&class="macro">#x27;)
plt.xlabel(&class="macro">#x27;class="num">5 Period RSI&class="macro">#x27;)
plt.ylabel(&class="macro">#x27;class="num">5 Period RSI&class="macro">#x27;)
plt.grid()
class="macro">#Set the max levels we wish to check
ALPHA_LEVELS = class="num">10
TOL_LEVELS  = class="num">10
class="macro">#DataFrame labels
r_c = &class="macro">#x27;TOL_LEVEL_&class="macro">#x27;
r_r = &class="macro">#x27;ALHPA_LEVEL_&class="macro">#x27;
results_columns = []
results_rows = []
for c in range(TOL_LEVELS):
    n_c = r_c + str(c)
    n_r = r_r + str(c)
    results_columns.append(n_c)
    results_rows.append(n_r)
class="macro">#Create a DataFrame to store our results
results = pd.DataFrame(columns=results_columns,index=results_rows)
class="macro">#Cross validate our model
for i in range(TOL_LEVELS):
    tol = class="num">10 ** (-i)
    error = []
    for j in range(ALPHA_LEVELS):
        class="macro">#Set alpha
        alpha = class="num">10 ** (-j)
        
        class="macro">#Its good practice to generally check the class="num">0 case
        if(i == class="num">0 & j == class="num">0):
            model = Ridge(alpha=j,tol=i,solver=&class="macro">#x27;sparse_cg&class="macro">#x27;)
        class="macro">#Otherwise use a class="type">float
        model = Ridge(alpha=alpha,tol=tol,solver=&class="macro">#x27;sparse_cg&class="macro">#x27;)
        class="macro">#Store the error levels
        error.append(np.mean(np.abs(cross_val_score(model,data.loc[:,[ &class="macro">#x27;Diff RSI class="num">5&class="macro">#x27;,
         &class="macro">#x27;Diff RSI class="num">10&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">15&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">20&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">25&class="macro">#x27;,
         &class="macro">#x27;Diff RSI class="num">30&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">35&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">40&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">45&class="macro">#x27;,
         &class="macro">#x27;Diff RSI class="num">50&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">55&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">60&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">65&class="macro">#x27;,
         &class="macro">#x27;Diff RSI class="num">70&class="macro">#x27;,]],data[&class="macro">#x27;Return&class="macro">#x27;],cv=tscv))))
    
    class="macro">#Record the error levels
    results.iloc[:,i] = error
    
results
    
class="kw">import plotly.graph_objects as go
fig = go.Figure(data =
    go.Contour(
        z=results,
        colorscale=&class="macro">#x27;bluered&class="macro">#x27;
    ))
fig.update_layout(
    width = class="num">600,
    height = class="num">400,
    title=&class="macro">#x27;Contour Plot Of Our Error Forecasting EURUSD Using Grid Search &class="macro">#x27;
)
fig.show()
class="macro">#Let&class="macro">#x27;s visualize the importance of each column
model = Ridge(alpha=class="num">0,tol=class="num">0,solver=&class="macro">#x27;sparse_cg&class="macro">#x27;)
model.fit(data.loc[:,[ &class="macro">#x27;Diff RSI class="num">5&class="macro">#x27;,
     &class="macro">#x27;Diff RSI class="num">10&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">15&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">20&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">25&class="macro">#x27;,
     &class="macro">#x27;Diff RSI class="num">30&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">35&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">40&class="macro">#x27;, &class="macro">#x27;Diff RSI class="num">45&class="macro">#x27;,

◍ 用交叉验证给 RSI 阈值找最优解

训练好的 Ridge 模型在特征重要性图上明显偏向 25 周期 RSI,但落到 EURUSD 实盘输入时,55 周期差分 RSI(Diff RSI 55)才是阈值扫描的主战场。 下面这段 Python 把 Diff RSI 55 的高低穿越规则写成目标函数,用时间序列交叉验证的均值绝对误差当评分:阈值越合理,error 越低。 def objective(x): data = pd.read_csv("EURUSD RSI Algorithmic Input Selection.csv") data['0'] = 0 data['1'] = 0 data['2'] = 0 HORIZON = 10 data['Return'] = data['True Close'].shift(-HORIZON) - data['True Close'] data.dropna(subset=['Return'],inplace=True) data.iloc[data['Diff RSI 55'] > x[0],12] = 1 data.iloc[data['Diff RSI 55'] < x[1],13] = 1 data.iloc[(data['Diff RSI 55'] < x[0]) & (data['RSI 55'] > x[1]),14] = 1 model = Ridge(alpha=0,tol=0,solver='sparse_cg') error = np.mean(np.abs(cross_val_score(model,data.iloc[:,12:15],data['Return'],cv=tscv))) return(error) 先跑 objective([0,0]) 和 objective([70,-30]) 会发现,传统「超买 70 / 超卖 30」的硬切法误差偏高,属于低效规则。 把阈值按 10 的倍数从 0 扫到 9,记录每次 error 并标出最低点;再细化到 20 档逐整数扫描,最低误差对应的索引就是最优阈值中心。以 0.5 倍 Diff RSI 55 标准差画红虚线,圈出稳健区间——外汇与贵金属波动剧烈,这套扫描仅降低模型误差,不预示方向,实盘仍属高风险。

MQL5 / C++
def objective(x):
    data = pd.read_csv("EURUSD RSI Algorithmic Input Selection.csv")
    data[&class="macro">#x27;class="num">0&class="macro">#x27;] = class="num">0
    data[&class="macro">#x27;class="num">1&class="macro">#x27;] = class="num">0
    data[&class="macro">#x27;class="num">2&class="macro">#x27;] = class="num">0
    HORIZON = class="num">10
    data[&class="macro">#x27;Return&class="macro">#x27;] = data[&class="macro">#x27;True Close&class="macro">#x27;].shift(-HORIZON) - data[&class="macro">#x27;True Close&class="macro">#x27;]
    data.dropna(subset=[&class="macro">#x27;Return&class="macro">#x27;],inplace=True)
    data.iloc[data[&class="macro">#x27;Diff RSI class="num">55&class="macro">#x27;] > x[class="num">0],class="num">12] = class="num">1
    data.iloc[data[&class="macro">#x27;Diff RSI class="num">55&class="macro">#x27;] < x[class="num">1],class="num">13] = class="num">1
    data.iloc[(data[&class="macro">#x27;Diff RSI class="num">55&class="macro">#x27;] < x[class="num">0]) & (data[&class="macro">#x27;RSI class="num">55&class="macro">#x27;] > x[class="num">1]),class="num">14] = class="num">1
    model = Ridge(alpha=class="num">0,tol=class="num">0,solver=&class="macro">#x27;sparse_cg&class="macro">#x27;)
    error = np.mean(np.abs(cross_val_score(model,data.iloc[:,class="num">12:class="num">15],data[&class="macro">#x27;Return&class="macro">#x27;],cv=tscv)))
    class="kw">return(error)

「用 RSI 差值分布找最优阈值」

这段 Python 片段把 EURUSD 的 'Diff RSI 55' 序列当成分布来处理,用最小误差点加减一个标准差系数来标定上下边界。plt.axvline 画出的红线位置是 results.index(min(results)) 减去 coef 乘上 Diff RSI 55 的标准差,直观看就是分布里最优点向左偏移的那条临界线。 def explanation(x) 里先读入 'EURUSD RSI Algorithmic Input Selection.csv',把 '0''1''2' 三列置零,再算 True Close 向后平移 10 根(HORIZON=10)的 Return 作为标签,dropna 去掉空值。 接着按 x[0]、x[1] 给三列打 1:Diff RSI 55 大于 x[0] 标 12 列,小于 x[1] 标 13 列,同时 Diff RSI 55 小于 x[0] 且 RSI 55 大于 x[1] 标 14 列。Ridge(alpha=0) 拿这三列去拟合 Return,返回系数。 opt=9 时 print(explanation([9,-9])) 跑出来的就是这组阈值下的模型系数,你可以直接换 opt 值看系数怎么变,外汇EURUSD这类品种波动大、杠杆高,回测结论只代表历史样本,实盘可能失效。

MQL5 / C++
plt.axvline(-(results.index(min(results)) + (coef * np.std(data[&class="macro">#x27;Diff RSI class="num">55&class="macro">#x27;]))),class="type">color=&class="macro">#x27;red&class="macro">#x27;)
plt.title("Visualizing our Optimal Point in The Distribution")
results.index(min(results)) + ( coef * np.std(data[&class="macro">#x27;Diff RSI class="num">55&class="macro">#x27;]))
results.index(min(results)) - (coef * np.std(data[&class="macro">#x27;Diff RSI class="num">55&class="macro">#x27;]))
def explanation(x):
    data = pd.read_csv("EURUSD RSI Algorithmic Input Selection.csv")
    data[&class="macro">#x27;class="num">0&class="macro">#x27;] = class="num">0
    data[&class="macro">#x27;class="num">1&class="macro">#x27;] = class="num">0
    data[&class="macro">#x27;class="num">2&class="macro">#x27;] = class="num">0
    HORIZON = class="num">10
    data[&class="macro">#x27;Return&class="macro">#x27;] = data[&class="macro">#x27;True Close&class="macro">#x27;].shift(-HORIZON) - data[&class="macro">#x27;True Close&class="macro">#x27;]
    data.dropna(subset=[&class="macro">#x27;Return&class="macro">#x27;],inplace=True)
    data.iloc[data[&class="macro">#x27;Diff RSI class="num">55&class="macro">#x27;] > x[class="num">0],class="num">12] = class="num">1
    data.iloc[data[&class="macro">#x27;Diff RSI class="num">55&class="macro">#x27;] < x[class="num">1],class="num">13] = class="num">1
    data.iloc[(data[&class="macro">#x27;Diff RSI class="num">55&class="macro">#x27;] < x[class="num">0]) & (data[&class="macro">#x27;RSI class="num">55&class="macro">#x27;] > x[class="num">1]),class="num">14] = class="num">1
    class="macro">#Calculate or RMSE When using those levels
    model = Ridge(alpha=class="num">0,tol=class="num">0,solver=&class="macro">#x27;sparse_cg&class="macro">#x27;)
    model.fit(data.iloc[:,class="num">12:class="num">15],data[&class="macro">#x27;Return&class="macro">#x27;])
    class="kw">return(model.coef_.copy())
opt = class="num">9
print(explanation([opt,-opt]))

把RSI模型落进EA骨架

顺着前几节对 EURUSD 日线 RSI 关系的梳理,下一步是把这套认知写成可跑的 MT5 程序。先钉死系统常量:RSI 周期 55、时间帧 PERIOD_D1、价格源 PRICE_CLOSE、缓冲 20 根,交易量直接取品种最小手数,避免手动填仓引发风控告警。 全局层只留必要句柄与变量——CTrade 实例、rsi_55 对象、last_value、count 以及两条均线数组 ma_o/ma_c,再加一个 trade_sl 专门扛动态止损。事件入口 OnInit 只做一件事:调 setup() 把指标句柄和计数器归零,后面更新函数再周期性刷系统变量并判断开仓或挪止损。 回测用 2022-01-01 到 2025-03 的日线,删掉与训练重叠的部分以近似实盘。测试模式选「基于真实 Tick 的每个 Tick」,夏普 0.92、预期回报 2.49、净利 151.87 美元、胜率 57.38%,但 61 笔里仅 4 笔多单——重度偏空是这版 EA 的明显畸变,后续要回图表查偏差源。外汇与贵金属杠杆高,回测甜美不代表实盘能复现,须以小资金验证。 下面这段是初始化骨架,逐行看:前几行宏定义常量;#include 拉入自写 RSI 库与系统交易类;全局区声明交易对象与指标实例;OnInit 仅调用 setup 并返回成功。复制进 MQ5 编译器即可接着补 update / trade rules。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  Algorithmic Input Selection.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                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define RSI_PERIOD class="num">55
class="macro">#define RSI_TIME_FRAME PERIOD_D1
class="macro">#define SYSTEM_TIME_FRAME PERIOD_D1
class="macro">#define RSI_PRICE   PRICE_CLOSE
class="macro">#define RSI_BUFFER_SIZE class="num">20
class="macro">#define TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load our RSI library                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <VolatilityDoctor\Indicators\RSI.mqh>
class="macro">#include <Trade\Trade.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTrade Trade;
RSI     rsi_55(Symbol(),RSI_TIME_FRAME,RSI_PERIOD,RSI_PRICE);
class="type">class="kw">double last_value;
class="type">int     count;
class="type">int     ma_o_handler,ma_c_handler;
class="type">class="kw">double ma_o[],ma_c[];
class="type">class="kw">double trade_sl;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   setup();
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 从 OnDeinit 到仓位管理的事件链

EA 卸载时先把两条均线指标句柄交还给系统,避免 MT5 终端残留句柄导致下次加载报 'indicator already loaded' 类错误。OnDeinit 里只做 IndicatorRelease(ma_c_handler) 与 IndicatorRelease(ma_o_handler),不在这里平仓,平仓逻辑留给策略自身控制。 行情驱动全压在 OnTick 调用 update(),update() 用静态变量 time_stamp 对比 iTime(Symbol(),SYSTEM_TIME_FRAME,0) 判断是否进入新 K 线。只有时间戳变化才重取 CopyBuffer 的 1 根均线值,并把无持仓时的 RSI 初值通过 rsi_55.GetReadingAt(RSI_BUFFER_SIZE-1) 写入 last_value,count 置 1 后不再重复初始化。 持仓后就走 manage_setup():先抓 bid/ask,再用 PositionSelect 读出现仓的 SL/TP。新止损按 '原 TP 大于原 SL 则为买仓,新 SL=bid-trade_sl;否则为卖仓,新 SL=ask+trade_sl' 推算,新 TP 对称反向。外汇与贵金属杠杆高,SL/TP 重算若间距过小可能被 broker 拒单,建议先在策略测试器用 2023 年 XAUUSD 的 M15 跑一遍看成交率。

MQL5 / C++
class=class="str">"cmt">//| Expert deinitialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
   IndicatorRelease(ma_c_handler);
   IndicatorRelease(ma_o_handler);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   update();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update our system variables                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update(class="type">void)
  {
   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;
      CopyBuffer(ma_c_handler,class="num">0,class="num">0,class="num">1,ma_c);
      CopyBuffer(ma_o_handler,class="num">0,class="num">0,class="num">1,ma_o);
      if((count == class="num">0) && (PositionsTotal() == class="num">0))
        {
         rsi_55.SetIndicatorValues(RSI_BUFFER_SIZE,true);
         last_value = rsi_55.GetReadingAt(RSI_BUFFER_SIZE - class="num">1);
         count = class="num">1;
        }
      if(PositionsTotal() == class="num">0)
         check_signal();
      if(PositionsTotal() > class="num">0)
         manage_setup();
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Manage our open trades                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void manage_setup(class="type">void)
  {
   class="type">class="kw">double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
   class="type">class="kw">double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
   if(PositionSelect(Symbol()))
     {
      class="type">class="kw">double current_sl = PositionGetDouble(POSITION_SL);
      class="type">class="kw">double current_tp = PositionGetDouble(POSITION_TP);
      class="type">class="kw">double new_sl = (current_tp > current_sl) ? (bid-trade_sl) : (ask+trade_sl);
      class="type">class="kw">double new_tp = (current_tp < current_sl) ? (bid+trade_sl) : (ask-trade_sl);
      class=class="str">"cmt">//--- Buy setup
      if((current_tp > current_sl) && (new_sl < current_sl))

「信号判定与全局变量的落地写法」

这段逻辑把 RSI 读数的跳变和两条 2 周期 EMA(收盘、开盘)的上下关系绑在一起,作为开仓触发条件。setup 里把 trade_sl 写死成 1.5e-2(即 150 点),外汇与贵金属杠杆高,这种固定止损距离在黄金上可能直接扫掉正常波动,实盘前建议在 MT5 里按品种重算。 check_signal 中 cp_lb=7.17、cp_ub=10.82 是 RSI(55) 前后读差的下上限阈值。注意原文第二个 if 的运算符 < ((last_value - current_reading) < cp_ub) 是语法错误,应改为 &&,否则编译会报类型不匹配,复制代码后必须先修这一行。 开仓只在 PositionsTotal()==0 时执行,避免加仓堆叠;卖单挂于 bid,止损 ask+trade_sl、止盈 ask-trade_sl,买单反之。想验证信号质量,把 Comment 输出的 Last Reading 和 Difference 调到图表左上角盯几天,看差值落进阈值带的频率再决定跟不跟。

MQL5 / C++
Trade.PositionModify(Symbol(),new_sl,new_tp);
class=class="str">"cmt">//--- Sell setup
if((current_tp < current_sl) && (new_sl > current_sl))
   Trade.PositionModify(Symbol(),new_sl,new_tp);
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Setup our global variables                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void setup(class="type">void)
  {
   ma_c_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,class="num">2,class="num">0,MODE_EMA,PRICE_CLOSE);
   ma_o_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,class="num">2,class="num">0,MODE_EMA,PRICE_OPEN);
   count       = class="num">0;
   last_value  = class="num">0;
   trade_sl    = class="num">1.5e-2;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if we have a trading setup                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_signal(class="type">void)
  {
   class="type">class="kw">double current_reading = rsi_55.GetCurrentReading();
   Comment("Last Reading: ",last_value,"\nDifference in Reading: ",(last_value - current_reading));
   class="type">class="kw">double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
   class="type">class="kw">double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
   class="type">class="kw">double cp_lb = class="num">7.17;
   class="type">class="kw">double cp_ub = class="num">10.82;
   if((((last_value - current_reading) <= -(cp_lb)) && ((last_value - current_reading) > (cp_ub)))|| ((((last_value - current_reading) > -(cp_lb))) && ((last_value - current_reading) < (cp_lb))))
     {
      if(ma_o[class="num">0] > ma_c[class="num">0])
        {
         if(PositionsTotal() == class="num">0)
           {
            Trade.Sell(TRADING_VOLUME,Symbol(),bid,(ask+trade_sl),(ask-trade_sl));
            count = class="num">0;
           }
        }
     }
   if(((last_value - current_reading) >= (cp_lb)) < ((last_value - current_reading) < cp_ub))
     {
      if(ma_c[class="num">0] < ma_o[class="num">0])
        {
         if(PositionsTotal() == class="num">0)
           {
            Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl));
            count = class="num">0;
           }
        }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef RSI_BUFFER_SIZE
class="macro">#undef RSI_PERIOD
class="macro">#undef RSI_PRICE
class="macro">#undef RSI_TIME_FRAME
class="macro">#undef SYSTEM_TIME_FRAME
class="macro">#undef TRADING_VOLUME
class=class="str">"cmt">//+------------------------------------------------------------------+

用更灵活的模型给EA补多头信号

图 18 的 EURUSD 月线截图里,两条红线框住 2009 年初到 2021 年末,绿线标出训练数据起点。2008 年开启的漫长熊市直接灌进 Ridge 模型,让它天然偏向空头仓位——这不是 bug,是数据分布使然。 与其拼命拉长历史样本去‘纠正’偏见,不如换一个更自由的 learner。把 Ridge 换成随机森林回归器,让 EA 去估未来 10 天汇率走高的概率,模型弹性上来后,多头触发点自然变多。 具体落地:随机森林输出看涨概率,越过 0.5 就开多;低于这条线,退回原 Ridge 策略执行。外汇与贵金属属高风险品种,概率阈值只是倾向性参考,实盘前请在 MT5 用 2009–2021 区间外推样本跑一遍确认信号分布。

◍ 用随机森林给欧元兑美元概率建模

把价格行为转成可训练样本后,下一步是在 Python 里用随机森林回归器拟合「Diff RSI 55」与未来价格动作之间的映射关系。这里自变量只取日线级别的 RSI(55) 差值,因变量是预先打标的 Target,模型不复杂,但足以输出未来 10 天出现看涨价格行为的概率倾向。 代码里先载入 sklearn 的 RandomForestRegressor 与 accuracy_score,用 data[['Diff RSI 55']] 作 X、data['Target'] 作 y,空参初始化后直接 fit。外汇与贵金属属高风险品种,这类概率仅作辅助参考,不代表方向必然成立。 模型训完要落地到 MT5,得转成 ONNX。用 skl2onnx 把模型转成原型,输入形状写死为 [1,1] 的浮点张量,target_opset=12,存盘文件名带品种周期标记:「EURUSD Diff RSI 55 D1 1 1.onnx」。 导出的 ONNX 建议用 netron 起本地服务看一眼图结构,确认输入输出节点形状无误再喂给 EA。图 19、图 20 展示的随机森林原型与预期属性一致,EA 端就能按这个接口去预测 10 日内的看涨概率。

MQL5 / C++
from sklearn.metrics class="kw">import accuracy_score
from sklearn.ensemble class="kw">import RandomForestRegressor
class="macro">#Independent variable
X = data[[&class="macro">#x27;Diff RSI class="num">55&class="macro">#x27;]]
class="macro">#Dependent variable
y = data[&class="macro">#x27;Target&class="macro">#x27;]
model = RandomForestRegressor()
model.fit(X,y)
class="kw">import onnx
from skl2onnx class="kw">import convert_sklearn
from skl2onnx.common.data_types class="kw">import FloatTensorType
inital_params = [("float_input",FloatTensorType([class="num">1,class="num">1]))]
onnx_proto = convert_sklearn(model=model,initial_types=inital_params,target_opset=class="num">12)
onnx.save(onnx_proto,"EURUSD Diff RSI class="num">55 D1 class="num">1 class="num">1.onnx")
class="kw">import netron
netron.start("EURUSD Diff RSI class="num">55 D1 class="num">1 class="num">1.onnx")

「把概率模型塞进EA后的真实账本」

把 EURUSD 的差分 RSI 概率模型导进 EA,核心动作就是加一组 ONNX 宏和全局句柄,让策略在多个代码分支里都能直接调模型,不必重复加载。下面这段只留了初始化与信号读取的增量部分,其余未改动的逻辑按原库沿用。 回测对比很说明问题:第二版在同样图 14、15 的条件下跑,资金曲线肉眼看和初版一致,但细分统计露了底。交易笔数从 61 笔升到 85 笔,增幅 39%;多头持仓从 4 笔跳到 42 笔,涨幅 950%。准确率由 57.38% 微降到 56.47%,约掉 1.59%,夏普与预期回报也略降。 额外风险显著抬高,而盈利统计只是小幅走弱,这种交换让策略倾向仍具备正向预期,但外汇品种杠杆高、价差跳变频繁,实盘前务必在 MT5 用历史数据重跑确认。 代码里 OnnxCreateFromBuffer 从 resource 载入二进制,OnnxSetInputShape / OutputShape 把张量锁成 [1,1],信号函数里直接把最新差分读数喂给 onnx_input[0],整个链路没有多余拷贝。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                     Algorithmic Input Selection.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 resources                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\Files\EURUSD Diff RSI class="num">55 D1 class="num">1 class="num">1.onnx" as class="type">uchar onnx_model_buffer[];
class="macro">#define ONNX_INPUTS class="num">1
class="macro">#define ONNX_OUTPUTS class="num">1
class="type">long   onnx_model;
vectorf onnx_output(class="num">1);
vectorf onnx_input(class="num">1);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Setup our global variables                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool setup(class="type">void)
  {
class=class="str">"cmt">//--- Setup our technical indicators
   ...
class=class="str">"cmt">//--- Create our ONNX model
   onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DATA_TYPE_FLOAT);
class=class="str">"cmt">//--- Validate the ONNX model
   if(onnx_model == INVALID_HANDLE)
     {
      class="kw">return(false);
     }
class=class="str">"cmt">//--- Define the I/O signature
   class="type">ulong onnx_param[] = {class="num">1,class="num">1};
   if(!OnnxSetInputShape(onnx_model,class="num">0,onnx_param))
      class="kw">return(false);
   if(!OnnxSetOutputShape(onnx_model,class="num">0,onnx_param))
      class="kw">return(false);
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if we have a trading setup                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_signal(class="type">void)
  {
   rsi_55.SetDifferencedIndicatorValues(RSI_BUFFER_SIZE,HORIZON,true);
   onnx_input[class="num">0] = (class="type">float) rsi_55.GetDifferencedReadingAt(RSI_BUFFER_SIZE - class="num">1);

把 ONNX 模型信号接进下单逻辑

这段 MT5 代码演示了随机森林模型如何直接驱动实盘下单。核心判断是:当 ONNX 输出的多头概率大于 0.5,且开盘均线低于收盘均线时,才触发 Buy 指令,属于典型的「模型信号 + 价格结构」双过滤。 外汇与贵金属杠杆高,这类自动下单逻辑在欧元美元日线周期上可能出现连续假信号,实盘前务必用策略测试器跑历史数据验证。 代码里加载的模型文件写死为 \Files\EURUSD Diff RSI 55 D1 1 1.onnx,说明特征工程基于周期 55 的 RSI 差值,输入维度只有 1 个、输出 1 个概率值。想换品种就得重新训练并替换 resource,不能指望同一模型跨货币对泛化。 常数里 RSI_BUFFER_SIZE 20HORIZON 10 暗示系统用最近 20 根日线的 RSI 差分作为上下文,预测未来 10 根日线方向。开 MT5 把 #resource 路径指向你自己的 onnx 文件,即可让小布这类 AIGC 工具替你跑通这套推断。

MQL5 / C++
class=class="str">"cmt">//--- Our Random forest model
if(!OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_input,onnx_output))
   Comment("Failed to obtain a forecast from our model!");
else
   {
   if(onnx_output[class="num">0] > class="num">0.5)
      if(ma_o[class="num">0] < ma_c[class="num">0])
         Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl));
   Print("Model Bullish Probabilty: ",onnx_output);
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Algorithmic Input Selection.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 resources                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#resource "\\Files\\EURUSD Diff RSI class="num">55 D1 class="num">1 class="num">1.onnx" as class="type">uchar onnx_model_buffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| System constants                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define RSI_PERIOD class="num">55
class="macro">#define RSI_TIME_FRAME PERIOD_D1
class="macro">#define SYSTEM_TIME_FRAME PERIOD_D1
class="macro">#define RSI_PRICE   PRICE_CLOSE
class="macro">#define RSI_BUFFER_SIZE class="num">20
class="macro">#define TRADING_VOLUME SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN)
class="macro">#define ONNX_INPUTS class="num">1
class="macro">#define ONNX_OUTPUTS class="num">1
class="macro">#define HORIZON class="num">10
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load our RSI library                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <VolatilityDoctor\Indicators\RSI.mqh>
class="macro">#include <Trade\Trade.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTrade Trade;

◍ EA 骨架与每根 K 线的状态刷新

这段 MT5 专家顾问的骨架把 RSI、两条均线句柄和 ONNX 模型句柄全部塞进全局变量,用 OnInit 调 setup() 完成指标绑定,OnDeinit 里释放均线句柄避免内存泄漏。 OnTick 只做一件事:每次报价进来就调 update()。真正有节流意义的是 update() 里的时间戳比对——用 iTime(Symbol(),SYSTEM_TIME_FRAME,0) 取当前周期时间,只有 time_stamp 变化(即新 K 线成型)才重算,避免每跳都跑全套逻辑。 新周期触发后,CopyBuffer 分别把收盘均线和开盘均线最新值拷进 ma_c[]、ma_o[]。首次运行且空仓时(count==0 && PositionsTotal()==0),RSI 对象一次性灌入 RSI_BUFFER_SIZE 长度的值并做差分处理,再把末尾读数存进 last_value、count 置 1。 之后每个新 K 线:空仓就 check_signal() 找入口,有仓就 manage_setup() 管止损仓位。外汇和贵金属波动大、滑点不可控,这套机制只是工程节流,信号本身仍可能连续亏损,实盘前请在策略测试器跑至少 3 个月 tick 数据。

MQL5 / C++
RSI     rsi_55(Symbol(),RSI_TIME_FRAME,RSI_PERIOD,RSI_PRICE);
class="type">class="kw">double last_value;
class="type">int     count;
class="type">int     ma_o_handler,ma_c_handler;
class="type">class="kw">double ma_o[],ma_c[];
class="type">class="kw">double trade_sl;
class="type">long   onnx_model;
vectorf onnx_output(class="num">1);
vectorf onnx_input(class="num">1);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   setup();
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">//---
   IndicatorRelease(ma_c_handler);
   IndicatorRelease(ma_o_handler);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   update();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update our system variables                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void update(class="type">void)
  {
   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;
      CopyBuffer(ma_c_handler,class="num">0,class="num">0,class="num">1,ma_c);
      CopyBuffer(ma_o_handler,class="num">0,class="num">0,class="num">1,ma_o);
      if((count == class="num">0) && (PositionsTotal() == class="num">0))
        {
         rsi_55.SetIndicatorValues(RSI_BUFFER_SIZE,true);
         rsi_55.SetDifferencedIndicatorValues(RSI_BUFFER_SIZE,HORIZON,true);
         last_value = rsi_55.GetReadingAt(RSI_BUFFER_SIZE - class="num">1);
         count = class="num">1;
        }
      if(PositionsTotal() == class="num">0)
         check_signal();
      if(PositionsTotal() > class="num">0)
         manage_setup();
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「持仓跟踪与模型初始化的实盘接法」

把挂着的单子管起来,核心不是频繁改止损,而是只在止损距离变得更优时才动手。manage_setup 里先抓 bid/ask,再用 PositionSelect 确认当前品种有无持仓;若有,就读出原 SL/TP,按多空方向算出新止损 new_sl,只有当新止损比原止损更靠安全侧才调用 PositionModify。 初始化阶段别漏掉 ONNX 句柄校验。setup 函数里用 OnnxCreateFromBuffer 从内存缓冲区建模型,若返回 INVALID_HANDLE 直接 false 退出;随后把输入和输出 shape 都设成 {1,1},这一步不通后面推理全废。 信号读取靠差分 RSI。check_signal 中 rsi_55 先填 55 长度缓冲并做差分,取末尾值 last_value,再拿当前读数 current_reading 做比对——外汇与贵金属波动剧烈,这套逻辑在实盘里可能只在特定时段出信号,属高风险用法,建议先开 MT5 用策略测试器跑通 shape 设置。

MQL5 / C++
class=class="str">"cmt">//| Manage our open trades                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void manage_setup(class="type">void)
  {
   class="type">class="kw">double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
   class="type">class="kw">double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
   if(PositionSelect(Symbol()))
     {
      class="type">class="kw">double current_sl = PositionGetDouble(POSITION_SL);
      class="type">class="kw">double current_tp = PositionGetDouble(POSITION_TP);
      class="type">class="kw">double new_sl = (current_tp > current_sl) ? (bid-trade_sl) : (ask+trade_sl);
      class="type">class="kw">double new_tp = (current_tp < current_sl) ? (bid+trade_sl) : (ask-trade_sl);
      class=class="str">"cmt">//--- Buy setup
      if((current_tp > current_sl) && (new_sl < current_sl))
         Trade.PositionModify(Symbol(),new_sl,new_tp);
      class=class="str">"cmt">//--- Sell setup
      if((current_tp < current_sl) && (new_sl > current_sl))
         Trade.PositionModify(Symbol(),new_sl,new_tp);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Setup our global variables                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool setup(class="type">void)
  {
class=class="str">"cmt">//--- Setup our technical indicators
   ma_c_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,class="num">2,class="num">0,MODE_EMA,PRICE_CLOSE);
   ma_o_handler = iMA(Symbol(),SYSTEM_TIME_FRAME,class="num">2,class="num">0,MODE_EMA,PRICE_OPEN);
   count       = class="num">0;
   last_value  = class="num">0;
   trade_sl    = class="num">1.5e-2;
class=class="str">"cmt">//--- Create our ONNX model
   onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DATA_TYPE_FLOAT);
class=class="str">"cmt">//--- Validate the ONNX model
   if(onnx_model == INVALID_HANDLE)
     {
      class="kw">return(false);
     }
class=class="str">"cmt">//--- Define the I/O signature
   class="type">ulong onnx_param[] = {class="num">1,class="num">1};
   if(!OnnxSetInputShape(onnx_model,class="num">0,onnx_param))
      class="kw">return(false);
   if(!OnnxSetOutputShape(onnx_model,class="num">0,onnx_param))
      class="kw">return(false);
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if we have a trading setup                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void check_signal(class="type">void)
  {
   rsi_55.SetDifferencedIndicatorValues(RSI_BUFFER_SIZE,HORIZON,true);
   last_value = rsi_55.GetReadingAt(RSI_BUFFER_SIZE - class="num">1);
   class="type">class="kw">double current_reading = rsi_55.GetCurrentReading();

把模型输出接进下单逻辑

这段代码片段展示了如何把随机森林与岭回归两类模型的判断,转成 MT5 实际的下单动作。外汇与贵金属杠杆高,任何模型信号都只是概率倾向,实盘前必须在策略测试器里跑过历史数据。 随机森林部分先取买价卖价,再把 RSI(55) 差分读数塞进 onnx_input[0],阈值卡在 0.5:输出大于该值且开盘均线下穿收盘均线时,才用 Trade.Buy 以卖价做多,止损止盈对称挂在 bid±trade_sl。 岭回归的触发区间写死了两个常数:cp_lb=7.17、cp_ub=10.82。读数差落进 [-7.17,7.17] 或 ≤-7.17 且 >10.82 的异常带时,若开盘均线高于收盘均线且空仓,就反向卖单;否则读数差 ≥7.17 且 <10.82 时,收盘均线低于开盘均线才买。 末尾一堆 #undef 只是清理宏定义,避免与其他文件撞名。复制时留意原文 Ridge Buy 那行逻辑运算符 <&& 混用疑似笔误,开 MT5 编译会报错,需按意图改成 && 再验证。

MQL5 / C++
  Comment("Last Reading: ",last_value,"\nDifference in Reading: ",(last_value - current_reading));
  class="type">class="kw">double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
  class="type">class="kw">double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
  class="type">class="kw">double cp_lb = class="num">7.17;
  class="type">class="kw">double cp_ub = class="num">10.82;
  class="type">class="kw">double onnx_input[class="num">0] = (class="type">float) rsi_55.GetDifferencedReadingAt(RSI_BUFFER_SIZE - class="num">1);
class=class="str">"cmt">//--- Our Random forest model
  if(!OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_input,onnx_output))
      Comment("Failed to obtain a forecast from our model!");
  else
   {
     if(onnx_output[class="num">0] > class="num">0.5)
       if(ma_o[class="num">0] < ma_c[class="num">0])
           Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl));
     Print("Model Bullish Probabilty: ",onnx_output);
   }
class=class="str">"cmt">//--- The trading rules we learned from our Ridge Regression Model
class=class="str">"cmt">//--- Ridge Regression Sell
  if((((last_value - current_reading) <= -(cp_lb)) && ((last_value - current_reading) > (cp_ub)))|| ((((last_value - current_reading) > -(cp_lb))) && ((last_value - current_reading) < (cp_lb))))
   {
     if(ma_o[class="num">0] > ma_c[class="num">0])
       {
        if(PositionsTotal() == class="num">0)
          {
           Trade.Sell(TRADING_VOLUME,Symbol(),bid,(ask+trade_sl),(ask-trade_sl));
           count = class="num">0;
          }
       }
   }
class=class="str">"cmt">//--- Ridge Regression Buy
  else
    if(((last_value - current_reading) >= (cp_lb)) < ((last_value - current_reading) < cp_ub))
      {
       if(ma_c[class="num">0] < ma_o[class="num">0])
         {
          if(PositionsTotal() == class="num">0)
            {
             Trade.Buy(TRADING_VOLUME,Symbol(),ask,(bid-trade_sl),(bid+trade_sl));
             count = class="num">0;
            }
         }
      }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#undef RSI_BUFFER_SIZE
class="macro">#undef RSI_PERIOD
class="macro">#undef RSI_PRICE
class="macro">#undef RSI_TIME_FRAME
class="macro">#undef SYSTEM_TIME_FRAME
class="macro">#undef TRADING_VOLUME
class="macro">#undef ONNX_INPUTS
class="macro">#undef ONNX_OUTPUTS
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 画得少,看得清

网格搜索配上统计模型,本质是把挑指标周期这件事从人工试错变成一次性算完。你不用再逐个回测 14、20、30 这些 RSI 周期,脚本跑一遍就把较优解列出来,EURUSD 上那几个 mq5 文件(Fetch_Data 5.25 KB、Testing_RSI_Class 10.19 KB)直接丢进 MT5 就能复现取数到验证的链路。 用新 RSI 阈值对比传统 70/30 的那套办法,价值不在『战胜市场』,而在于你清楚自己偏离惯例多少、样本内偏差有多大。Algorithmic_Input_Selection_2.mq5(8.19 KB)修掉的正是初版学来的偏误,打开看 diff 比看结论更有用。 外汇和贵金属杠杆高、滑点凶,任何『最优周期』都只是历史样本里的倾向,换一段行情就可能失效。把文件下下来跑一遍,比记住结论更实在。

把参数搜索交给小布盯盘
这些多周期 RSI 扫描与偏差建模的重复劳动,小布盯盘的 AIGC 已内置,打开对应品种页即可看到候选周期与阈值分布,你只管做最终决策。

常见问题

周期过长时价格均值回归被平滑掉,RSI 失去摆动空间,信号生成能力趋近于零,仅适合极长周期趋势过滤。
用滚动窗口做概率建模,观察不同子区间信号胜率衰减;若样本外区间掉得厉害,周期大概率只是拟合了噪声。
品种页的 AIGC 诊断可输出候选周期与水平倍数区间,但是否接实盘仍需你结合风控与品种波动特征判断,外汇贵金属高风险。
倍数越小信号越密、噪声越多;常见从 1.5 倍起测,结合 Python 建模看盈亏分布再收窄或放宽,没有统一最优。
可能,建议先 Python 离线程筛再写死到 MQL5 做轻量自适应,避免在 tick 里做全网格搜索拖慢执行。