基于Python和MQL5的特征工程(第一部分):为长期 AI 模型预测移动平均线·进阶篇
📘

基于Python和MQL5的特征工程(第一部分):为长期 AI 模型预测移动平均线·进阶篇

第 2/3 篇

「用网格扫描找澳日回归的最优参数窗」

这段脚本干的事很直接:把预测步长(Forecast Horizon)和差分周期(Differencing Period)铺成二维网格,对每个组合跑一次 evaluate 算拟合误差,最后用 contourf 把误差面画出来。白点标的是两个轴上各自最小误差的交叉位置,也就是肉眼可抄的较优参数。 注意它前后画了两张图,标题分别是「AUDJPY Daily Return」和「AUDJPY Daily Moving Average Return」——同样扫描逻辑,换了个收益口径,白点位置可能偏移,说明参数对标的序列敏感,不能一套通吃。 末尾的 3D surface 用 subplot_kw={"projection":"3d"} 建轴,fig.set_size_inches(8,8) 定正方画布,plot_surface 把 optimal_nn_res 当高度场。你本地跑时若 optimal_nn_res 没先算好会直接报空,建议先确认前面网格结果已落进这个变量。外汇与贵金属这类品种波动跳变多,网格寻优只给历史样本下的概率较优,实盘仍属高风险。

MQL5 / C++
    period   = y[i]
    for j in np.arange(class="num">0,y.shape[class="num">0]):
        results[i,j] = evaluate(look_ahead[j],period[j])
    class="kw">return(results)
res = objective(x,y)
res = np.abs(res)
plt.contourf(x,y,res,class="num">100,cmap="jet")
plt.plot(x_axis[res.min(axis=class="num">0).argmin()],y_axis[res.min(axis=class="num">1).argmin()],&class="macro">#x27;.&class="macro">#x27;,class="type">class="kw">color=&class="macro">#x27;white&class="macro">#x27;)
plt.ylabel("Differencing Period")
plt.xlabel("Forecast Horizon")
plt.title("Linear Regression Accuracy Forecasting AUDJPY Daily Return")
plt.contourf(x,y,res,class="num">100,cmap="jet")
plt.plot(x_axis[res.min(axis=class="num">0).argmin()],y_axis[res.min(axis=class="num">1).argmin()],&class="macro">#x27;.&class="macro">#x27;,class="type">class="kw">color=&class="macro">#x27;white&class="macro">#x27;)
plt.ylabel("Differencing Period")
plt.xlabel("Forecast Horizon")
plt.title("Linear Regression Accuracy Forecasting AUDJPY Daily Moving Average Return")
class="macro">#Create a surface plot
fig , ax = plt.subplots(subplot_kw={"projection":"3d"})
fig.set_size_inches(class="num">8,class="num">8)
ax.plot_surface(x,y,optimal_nn_res,cmap="jet")

◍ 用 wavelet 硬阈值给均线信号挤水分

线性变换只是特征工程的入门,往非线性走往往靠试错,没有公式能预告哪种变换当下最优。小波变换把序列同时投到时间与频率域,靠识别微小频率成分来分离噪声,低于阈值的系数直接压成 0,得到原始数据的稀疏表达。 相比 FFT 把信号拆成正弦余弦并一刀切滤掉高频,小波对高频域信号更友好,能以无监督方式保留真实波动、只剔噪声。想在 MT5 外做这套,先装 scikit-image 与 PyWavelets;纯 MQL5 从零写变换调试成本过高,接 Python 库反而快。 实测只对新浪均线类输入做变换、不动目标列:基准线性回归交叉验证均值 0.9935846835797412,小波硬阈值(BayesShrink + sym5 + 3 层)后降到 0.9082244556297641。准确率下滑说明去噪削掉了部分拟合用的高频成分,外汇与贵金属行情高风险,这种稀疏化是否利于实盘信号,需你自行回测确认。 硬阈值把噪声系数置 0,软阈值仅令其趋近 0;若想少丢细节可换软阈值重跑上面代码比对。

MQL5 / C++
class="macro">#Benchmark Score
np.mean(cross_val_score(LinearRegression(),data.loc[:,["MA"]],data["Target"]))
class="macro">#Wavelet denoising
data["Denoised"] = denoise_wavelet(
    data["MA"],
    method=&class="macro">#x27;BayesShrink&class="macro">#x27;,
    mode=&class="macro">#x27;hard&class="macro">#x27;,
    rescale_sigma=True,
    wavelet_levels = class="num">3,
    wavelet=&class="macro">#x27;sym5&class="macro">#x27;
    )
np.mean(cross_val_score(LinearRegression(),np.sqrt(np.log(data.loc[:,["Denoised"]])),data["Target"]))

用双指标状态化喂数据训练回归模型

直接从 MT5 终端抓行情,而不是用离线样本,才能保证训练时看到的 MA、RSI 数值和实盘机器人读到的一致。脚本里把移动平均线周期锁死在前面算出的理想值(示例用 40 期 SMA),同时拉取 RSI 读数,靠两个独立指标共同约束模型,行为可能比单指标预测更稳。 回测要有意义,得拿没见过的数据说话。原文做法是从生成的 CSV 里剔掉最近两年(2023–2024),只用此前约 22 年数据训练,这样跑 2023–2024 回测时,结果倾向反映样本外表现。外汇与贵金属杠杆高,样本外失效风险始终存在。 标记数据时把指标转成离散状态:RSI 分三档(>70、<30、中间),命中档置 1 其余 0;MA 只有上升 / 下降两态。Ridge 模型系数显示,RSI 超 70 时对应均值为 -0.1929 的价格变化,读数小于 70 时反而倾向上涨(系数 0.1482、0.0448),和经典「超买必跌」教条不完全一致。 MA 两态系数 array([-0.15572796, 0.15572796]) 说明:40 期 AUDJPY 日线 MA 连涨 40 根后倾向回落,连跌后倾向反弹,模型自己学出了均值回归。把 MA 当前状态喂进去,预测误差降到约 -0.0096,比单看 RSI 的 -0.0256 更紧。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 ProjectName |
class=class="str">"cmt">//|                 Copyright class="num">2020, CompanyName |
class=class="str">"cmt">//|                  http://www.companyname.net |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Gamuchirai Zororo Ndawana"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property script_show_inputs
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script Inputs                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">int size = class="num">100000; class=class="str">"cmt">//How much data should we fetch?
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Global variables                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int    ma_handler,rsi_handler;
class="type">class="kw">double ma_reading[],rsi_reading[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| On start function                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- Load indicator
ma_handler   = iMA(Symbol(),PERIOD_CURRENT,class="num">40,class="num">0,MODE_SMA,PRICE_CLOSE);
rsi_handler = iRSI(Symbol(),PERIOD_CURRENT,class="num">30,PRICE_CLOSE);
  
class=class="str">"cmt">//--- Load the indicator values
CopyBuffer(ma_handler,class="num">0,class="num">0,size,ma_reading);
CopyBuffer(rsi_handler,class="num">0,class="num">0,size,rsi_reading);
ArraySetAsSeries(ma_reading,true);

「把 MT5 导出的 MA+RSI 序列喂给回归模型」

MT5 端先按时间序列方向把 RSI 数组排好,再拼出带品种名与指标名的 CSV 文件,用 FileOpen 以 ANSI+CSV 模式写入,表头含 Time/Open/High/Low/Close/MA/RSI,循环从 size 递减到 0 把每根 K 线的 iTime、iOpen 等和预存的 ma_reading、rsi_reading 逐行落盘,最后 FileClose 收尾。 读回时用 pandas 加载 'Market Data AUDJPY MA RSI As Series.csv',砍掉末尾 365*2=730 根作为预留验证集,仅留 iloc[365:-(365*2),:] 做训练样本。 特征构造上,MA 状态用当前 MA 与 40 根前 MA 比较:大于置 MA1=1、否则 MA2=1;RSI 按 <30、>70、30~70 三档分别打 RSI1/RSI2/RSI3 标签。目标变量取 22 根后的 Close 差与 MA 差,模拟中期偏移。 数据清洗丢弃 NaN 并再截掉前 40 根(避免 shift(40) 引入的空缺),reset_index 后送 Ridge 回归。模型从样本里学到:RSI 读数低于 30 时,后续价格倾向走弱;其余区间则倾向回升。外汇与贵金属波动剧烈,该结论仅基于历史样本,实盘可能失效,务必用小资金验证。

MQL5 / C++
ArraySetAsSeries(rsi_reading,true);
class=class="str">"cmt">//--- File name
  class="type">class="kw">string file_name = "Market Data " + Symbol() +" MA RSI " +  " As Series.csv";
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">0;i--)
   {
     if(i == size)
      {
       FileWrite(file_handle,"Time","Open","High","Low","Close","MA","RSI");
      }
     else
      {
       FileWrite(file_handle,iTime(Symbol(),PERIOD_CURRENT,i),
                  iOpen(Symbol(),PERIOD_CURRENT,i),
                  iHigh(Symbol(),PERIOD_CURRENT,i),
                  iLow(Symbol(),PERIOD_CURRENT,i),
                  iClose(Symbol(),PERIOD_CURRENT,i),
                  ma_reading[i],
                  rsi_reading[i]
                  );
      }
   }
class=class="str">"cmt">//--- Close the file
  FileClose(file_handle);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#Read in the data
data = pd.read_csv("Market Data AUDJPY MA RSI As Series.csv")
class="macro">#Let&class="macro">#x27;s drop the last two years of data. We&class="macro">#x27;ll use that to validate our model in the back test
data = data.iloc[class="num">365:-(class="num">365 * class="num">2),:]
data
class="macro">#MA States
data["MA class="num">1"] = class="num">0
data["MA class="num">2"] = class="num">0
data.loc[data["MA"] > data["MA"].shift(class="num">40),"MA class="num">1"] = class="num">1
data.loc[data["MA"] <= data["MA"].shift(class="num">40),"MA class="num">2"] = class="num">1
class="macro">#RSI States
data["RSI class="num">1"] = class="num">0
data["RSI class="num">2"] = class="num">0
data["RSI class="num">3"] = class="num">0
data.loc[data["RSI"] < class="num">30,"RSI class="num">1"] = class="num">1
data.loc[data["RSI"] > class="num">70,"RSI class="num">2"] = class="num">1
data.loc[(data["RSI"] >= class="num">30) & (data["RSI"] <= class="num">70),"RSI class="num">3"] = class="num">1
class="macro">#Target
data["Target"]    = data["Close"].shift(-class="num">22) - data["Close"]
data["MA Target"] = data["MA"].shift(-class="num">22) - data["MA"]
class="macro">#Clean up the data
data = data.dropna()
data = data.iloc[class="num">40:,:]
data = data.reset_index(drop=True)
from sklearn.linear_model class="kw">import Ridge
from sklearn.model_selection class="kw">import TimeSeriesSplit,cross_val_score
class="macro">#Our model can suggest optimal ways of using the RSI indicator
class="macro">#Our model has learned that on average price tends to fall the RSI reading is less than class="num">30 and increases otherwises
model = Ridge()

◍ 用岭回归给 RSI 和均线称重

把 RSI 三个周期(RSI 1/2/3)当作特征喂给 Ridge 模型去拟合目标列,再打印 coef_ 看每个周期对方向的线性权重。交叉验证用时间序列切分(tscv),RSI 状态的平均 cv 分数能反映这组特征在样本外的稳定度。 模型学到一个倾向:当 RSI 读数低于 30 时,价格后续倾向于走弱;高于该阈值则倾向回升。这不是确定性规律,外汇与贵金属市场高风险,阈值效应会随品种和周期漂移。 换用两条均线(MA 1/2)重复同样流程,fit 后看 coef_ 正负与量级,再跑一次时间序列交叉验证。对比两组 cv 均值,能判断当前样本里哪类因子对短期方向更有解释力,开 MT5 导出对应周期数据即可复算。

MQL5 / C++
model.fit(data.loc[:,[“RSI class="num">1”,“RSI class="num">2”,“RSI class="num">3”]] , data[“Target”])
model.coef_
class="macro">#RSI state
np.mean(cross_val_score(Ridge(),data.loc[:,[“RSI class="num">1”,“RSI class="num">2”,“RSI class="num">3”]] , data[“Target”],cv=tscv))
class="macro">#Our model can suggest optimal ways of using the RSI indicator
class="macro">#Our model has learned that on average price tends to fall the RSI reading is less than class="num">30 and increases otherwises
model = Ridge()
model.fit(data.loc[:,[“MA class="num">1”,“MA class="num">2”]] , data[“Target”])
model.coef_
class="macro">#MA state
np.mean(cross_val_score(Ridge(),data.loc[:,[“MA class="num">1”,“MA class="num">2”]] , data[“Target”],cv=tscv))

把 Ridge 模型落盘成 ONNX

前面已经用网格搜索敲定了移动平均线预测的最佳输入窗口(MA 1、MA 2 两列滞后特征),下一步是把训练好的模型从 Python 环境里剥出来,换成 ONNX 这种与语言无关的通用格式。这样一来,MetaTrader 5 里的 MQL5 侧就能直接加载 .onnx 文件做推理,不必重跑训练流程。 具体导出时,RSI 模型用了 3 个输入特征(RSI 1/2/3),MA 模型用了 2 个输入特征(MA 1/2),两者都用 Ridge 回归在全量数据上拟合。target_opset=12 是转换时的算子集版本,文件名里带的 AUDJPY D1 标明这是澳日日线周期的产物——外汇与贵金属杠杆品种波动剧烈,实盘加载前务必在 MT5 策略测试器用历史数据回测验证推断一致性。 代码里 initial_types 的 FloatTensorType([1, 3]) 和 [1, 2] 直接对应模型输入张量的形状,写错一个维度,MQL5 端用 OnnxRunModel 调用就会报输入不匹配。导出后硬盘上会多两个文件:AUDJPY D1 RSI AI F22 P40.onnx 与 AUDJPY D1 MA AI F22 P40.onnx,这就是给盯盘 EA 喂信号的实际权重。

MQL5 / C++
class="macro">#Fetch clean data
new_data = clean_data(class="num">140,class="num">130)
class="kw">import onnx
from  skl2onnx class="kw">import convert_sklearn
from  skl2onnx.common.data_types class="kw">import FloatTensorType
class="macro">#First we will class="kw">export the RSI model
rsi_model = Ridge()
rsi_model.fit(data.loc[:,[&class="macro">#x27;RSI class="num">1&class="macro">#x27;,&class="macro">#x27;RSI class="num">2&class="macro">#x27;,&class="macro">#x27;RSI class="num">3&class="macro">#x27;]],data.loc[:,&class="macro">#x27;Target&class="macro">#x27;])
class="macro">#Finally we will class="kw">export the MA model
ma_model = Ridge()
ma_model.fit(data.loc[:,[&class="macro">#x27;MA class="num">1&class="macro">#x27;,&class="macro">#x27;MA class="num">2&class="macro">#x27;]],data.loc[:,&class="macro">#x27;MA Target&class="macro">#x27;])
initial_types = [(&class="macro">#x27;float_input&class="macro">#x27;, FloatTensorType([class="num">1, class="num">3]))]
onnx.save(convert_sklearn(rsi_model,initial_types=initial_types,target_opset=class="num">12),"AUDJPY D1 RSI AI F22 P40.onnx")
initial_types = [(&class="macro">#x27;float_input&class="macro">#x27;, FloatTensorType([class="num">1, class="num">2]))]
onnx.save(convert_sklearn(ma_model,initial_types=initial_types,target_opset=class="num">12),"AUDJPY D1 MA AI F22 P40.onnx")

常见问题

建议以窗长 20~200、步长 10 做粗扫,再用步长 2 在较优区间细扫,避免算力浪费。
可先对信号做离散小波分解到 3 层,阈值取各层噪声标准差的 1.2 倍,再硬截零,回测看平滑度。
可以,小布能按你设定的状态化规则把均线和 RSI 序列转成特征矩阵,直接导出供训练调用。
从 0.01 到 10 做对数网格交叉验证,选验证误差拐点的 alpha,通常 0.1~1 区间较稳。
要,推理端需按训练时的均值方差反归一化,否则输出的均线预测值量纲会错。