将 MQL5 与数据处理包集成(第 1 部分):高级数据分析和统计处理·进阶篇
📘

将 MQL5 与数据处理包集成(第 1 部分):高级数据分析和统计处理·进阶篇

第 2/2 篇

用 EDA 摸清 XAUUSD 数据的底

探索性数据分析(EDA)不是为了炫图,而是先搞清楚手里的历史报价到底长什么样。对黄金小时线这类高频序列,先确认缺失值、异常跳空和列类型,再做分布观察,否则后面训练的模型只会学出一地鸡毛。 下面这段 Python 脚本直接读入 XAUUSD 的 H1 历史 csv,先打印 info 和 describe 看字段概况,再画收盘价走势,最后用 TA-Lib 算 14 周期 RSI 做特征。注意它只拿 RSI 单列去预测下一根收盘价,属于刻意压到最简化来验证基线。 相关矩阵是 EDA 里不能跳的一步:接近 1 是同向强相关(如开盘与收盘),接近 -1 是反向强相关(如放量与价跌的概率倾向),接近 0 则基本无线性关联。黄金短线里 RSI 与隔根收盘往往相关系数偏低,单特征模型容易扑街。 回测现象很直白——预测值与实际值明显错位,MSE 打印出来通常偏大。常见病因就四个:样本太小、过拟合、欠拟合、特征工程太糙。诊断时优先查数据干净度,再用 MSE / MAE 量化,别凭眼睛猜。 外汇与贵金属杠杆高、滑点凶,这类实验只在 MT5 导出的历史数据上做离线验证,任何‘模型能预测’都只是概率倾向,实盘前必须重采样交叉核对。

MQL5 / C++
class="kw">import seaborn as <span class="keyword">as</span> sns
class="kw">import matplotlib.pyplot <span class="keyword">as</span> plt
class="kw">import warnings
warnings.filterwarnings(<span class="class="type">class="kw">string">"ignore"</span>)
<span class="keyword">for</span> i <span class="keyword">in</span> data.select_dtypes(include=<span class="class="type">class="kw">string">"number"</span>).columns:
&nbsp;&nbsp;&nbsp;&nbsp;sns.histplot(data=data, x=i)
&nbsp;&nbsp;&nbsp;&nbsp;plt.show()
class="kw">import pandas <span class="keyword">as</span> pd
class="kw">import numpy <span class="keyword">as</span> np
class="kw">import matplotlib.pyplot <span class="keyword">as</span> plt
class="kw">import seaborn <span class="keyword">as</span> sns
<span class="keyword">from</span> sklearn.model_selection class="kw">import train_test_split
<span class="keyword">from</span> sklearn.ensemble class="kw">import RandomForestRegressor
<span class="keyword">from</span> sklearn.metrics class="kw">import r2_score, mean_squared_error
class="kw">import talib <span class="keyword">as</span> ta&nbsp;&nbsp;# Technical Analysis library
# Load the data
file_path = <span class="class="type">class="kw">string">&class="macro">#x27;/home/int_junkie/Documents/ML/XAUUSD.m_H1_historical.csv&class="macro">#x27;</span>
data = pd.read_csv(file_path, delimiter=<span class="class="type">class="kw">string">&class="macro">#x27;\t&class="macro">#x27;</span>)
# Exploratory Data Analysis(EDA)
print(data.info())
print(data.describe())
# Visualize the closing price
plt.figure(figsize=(<span class="number">class="num">12</span>, <span class="number">class="num">6</span>))
plt.plot(data[<span class="class="type">class="kw">string">&class="macro">#x27;&lt;CLOSE&gt;&class="macro">#x27;</span>])
plt.title(<span class="class="type">class="kw">string">&class="macro">#x27;XAUUSD Closing Price&class="macro">#x27;</span>)
plt.xlabel(<span class="class="type">class="kw">string">&class="macro">#x27;&lt;DATE&gt;&class="macro">#x27;</span>)
plt.ylabel(<span class="class="type">class="kw">string">&class="macro">#x27;Price&class="macro">#x27;</span>)
plt.show()
# Feature Engineering
data[<span class="class="type">class="kw">string">&class="macro">#x27;RSI&class="macro">#x27;</span>] = ta.RSI(data[<span class="class="type">class="kw">string">&class="macro">#x27;&lt;CLOSE&gt;&class="macro">#x27;</span>], timeperiod=<span class="number">class="num">14</span>)
# Drop rows with missing values
data.dropna(inplace=True)
# Define target variable
data[<span class="class="type">class="kw">string">&class="macro">#x27;Target&class="macro">#x27;</span>] = data[<span class="class="type">class="kw">string">&class="macro">#x27;&lt;CLOSE&gt;&class="macro">#x27;</span>].shift(-<span class="number">class="num">1</span>)
data.dropna(inplace=True)
# Split the data
X = data[[<span class="class="type">class="kw">string">&class="macro">#x27;RSI&class="macro">#x27;</span>]]&nbsp;&nbsp;# Only use RSI <span class="keyword">as</span> the feature
y = data[<span class="class="type">class="kw">string">&class="macro">#x27;Target&class="macro">#x27;</span>]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=<span class="number">class="num">0.2</span>, shuffle=False)
# Model Development
model = RandomForestRegressor(n_estimators=<span class="number">class="num">100</span>, random_state=<span class="number">class="num">42</span>)
model.fit(X_train, y_train)
# Predictions
y_pred = model.predict(X_test)
# Evaluate the model
mse = mean_squared_error(y_test, y_pred)
print(f<span class="class="type">class="kw">string">&class="macro">#x27;Mean Squared Error: {mse}&class="macro">#x27;</span>)
# Visualize the predictions
plt.figure(figsize=(<span class="number">class="num">12</span>, <span class="number">class="num">6</span>))
plt.plot(y_test.index, y_test, label=<span class="class="type">class="kw">string">&class="macro">#x27;Actual Values&class="macro">#x27;</span>)
plt.plot(y_test.index, y_pred, label=<span class="class="type">class="kw">string">&class="macro">#x27;Predicted Values&class="macro">#x27;</span>)
plt.xlabel(<span class="class="type">class="kw">string">&class="macro">#x27;Samples&class="macro">#x27;</span>)
plt.ylabel(<span class="class="type">class="kw">string">&class="macro">#x27;TARGET_CLOSE&class="macro">#x27;</span>)
plt.title(<span class="class="type">class="kw">string">&class="macro">#x27;Actual vs Predicted Values&class="macro">#x27;</span>)
plt.legend()
plt.show()

「让 Python 预测脚本能被 MT5 抓输出」

模型训完保存后,真正接进交易流程的是一段 Python 预测脚本,而不是在 Notebook 里看准确率。它的职责很单一:读模型、读特征、出一个数,然后把这个数打到标准输出,等外部程序来收。 脚本靠命令行参数拿路径,sys.argv[1] 是模型文件(如 random_forest_model.pkl),sys.argv[2] 是特征文件(如 features.txt)。进脚本先 print 两个路径做调试,再用 os.path.exists 分别查文件,缺任何一个就报错并 sys.exit(1),避免 MT5 侧拿到空结果还当成信号。 特征文件按行读,每行 strip 掉空格后转 float 塞进列表;模型用 joblib.load 载入后,model.predict([features])[0] 取第一个预测值直接 print。这个 stdout 文本就是 MQL5 那边能用 Shell/管道捕获的字符串,外汇与贵金属杠杆高、模型错判会放大亏损,实盘前务必在策略测试器里先验证通路。 下面这段是可直接抄去改路径的原型,路径写死仅为示例,真实部署建议仍走参数传入。

MQL5 / C++
class="kw">import joblib
class="kw">import sys
class="kw">import os
model_path = sys.argv[/home/int_junkie/Documents/ML/random_forest_model.pkl]
features_path = sys.argv[/home/int_junkie/Documents/ML/features.txt]
print(f"Model path: {model_path}")
print(f"Features path: {features_path}")
if not os.path.exists(model_path):
    print(f"Error: Model file not found at {model_path}")
    sys.exit(class="num">1)
if not os.path.exists(features_path):
    print(f"Error: Features file not found at {features_path}")
    sys.exit(class="num">1)
model = joblib.load(model_path)
with open(features_path, &class="macro">#x27;r&class="macro">#x27;) as f:
    features = [class="type">float(line.strip()) for line in f]
prediction = model.predict([features])[class="num">0]
print(prediction)

◍ EA 里调 Python 模型的桥接写法

把训练好的随机森林模型接进 MT5,核心不在指标计算,而在让 MQL5 与 Python 两边用文件做中间人。OnInit 里先把模型路径和特征文件定死,再把特征从 txt 读进 double 数组,这一步只做一次,避免每跳一笔报价都重读磁盘。 OnTick 里先用 iBars 比对新旧 bar 数判断是不是新 K 线,不是新 bar 就跳过,避免同一根 K 线内反复跑脚本浪费 IO。新 bar 时才把当前收盘价写进 features.txt,然后拼出一条 python 命令,用 ShellExecuteA 拉起 predict.py 做推断。 脚本跑起来后必须 Sleep(1000) 等至少 1 秒,否则预测文件还没写完你就去读,大概率是空文件或上一根的结果。读文件前先判 INVALID_HANDLE,打不开就 Print 错误直接 return,别让 EA 带着脏信号下单。 阈值怎么切信号很直接:预测值大于阈值倾向出买讯,小于阈值倾向出卖讯。外汇和贵金属杠杆高、滑点跳空频繁,这套桥接方案只是把模型推断搬进终端,实盘胜率仍受样本外漂移影响,参数和阈值都得自己回测。

MQL5 / C++
class="type">int OnInit(){
  class=class="str">"cmt">// Load the model and feature names
  class="type">class="kw">string modelPath = "/home/int_junkie/Documents/ML/random_forest_model.pkl";
  class="type">class="kw">string featurePath = "/home/int_junkie/Documents/ML/features.txt";
  
  class=class="str">"cmt">// Your code to load the model(use appropriate library for pkl files)
  class=class="str">"cmt">// Initialize the features
  class="type">class="kw">double features[];
  class="type">int fileHandle = FileOpen(featurePath, FILE_READ | FILE_TXT);
  if (fileHandle != INVALID_HANDLE)
    {
      class="type">class="kw">string line;
      while(!FileIsEnding(fileHandle))
        {
         line = FileReadString(fileHandle);
         ArrayResize(features, ArraySize(features) + class="num">1);
         features[ArraySize(features) - class="num">1] = StringToDouble(line);
        }
      FileClose(fileHandle);
    }
  class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnTick(){
  class=class="str">"cmt">// Declare class="kw">static variables to retain values across function calls
  class="kw">static class="type">bool isNewBar = false;
  class="kw">static class="type">int prevBars = class="num">0;
  
  class=class="str">"cmt">// Get the current number of bars
  class="type">int newbar = iBars(_Symbol, _Period);
  
  class=class="str">"cmt">// Check if the number of bars has changed
  if (prevBars == newbar) {
      class=class="str">"cmt">// No new bar
      isNewBar = false;
  } else {
      class=class="str">"cmt">// New bar detected
      isNewBar = true;
      class=class="str">"cmt">// Update previous bars count to current
      prevBars = newbar;
  }
  
  class=class="str">"cmt">// Update the features based on current data
  class="type">class="kw">double features[];
  ArrayResize(features, class="num">1);
  features[class="num">0] = iClose(Symbol(), class="num">0, class="num">0);
  
  class=class="str">"cmt">// Write the features to a file
  class="type">int fileHandle = FileOpen("/home/int_junkie/Documents/ML/features.txt", FILE_WRITE | FILE_TXT);
  if (fileHandle != INVALID_HANDLE)
    {
      for (class="type">int i = class="num">0; i < ArraySize(features); i++)
        {
         FileWrite(fileHandle, DoubleToString(features[i]));
        }
      FileClose(fileHandle);
    }
  else
    {
      Print("Error: Cannot open features file for writing");
      class="kw">return;
    }
    
      class=class="str">"cmt">// Call the Python script to get the prediction
  class="type">class="kw">string command = "python /home/int_junkie/Documents/ML/predict.py /home/int_junkie/Documents/ML/random_forest_model.pkl /home/int_junkie/Documents/ML/features.txt";
  class="type">int result = ShellExecuteA(command);

MT5 接 Python 预测信号的落地写法

把外部模型跑完的结果接进 MT5,核心不是算法本身,而是文件握手那一下。上面这段逻辑先等 ShellExecuteA 调起 Python 脚本,失败就直接 Print 错误码并 return,不往下走,避免读到一个空文件还硬开仓。 文件读回靠 FileOpen 指向本地 prediction.txt,这里给的路径是 Linux 下的绝对目录,Windows 环境要改成自己的 MQL5/Files 映射路径,否则 INVALID_HANDLE 会稳定触发。Sleep(1000) 只是粗等,真实回测里 Python 推理超过 1 秒就会读到半截字符串,建议改成轮询文件修改时间。 pred_value 和阈值 0.0 比大小决定方向:大于阈值用 SYMBOL_ASK 加 normalized 点位算 sl/tp 走 BUY,小于阈值镜像走 SELL。stopLoss、takeProfit 以 _Point 倍数传入,外汇和贵金属点值不同,XAUUSD 的 _Point 是 0.01,EURUSD 是 0.00001,参数不分开调就会把止损放大十倍,属于实盘里容易爆的坑。 别把正态当圣经:阈值写死 0.0 只是占位,模型输出若做了 min-max 缩放,临界值更可能在 0.5 附近,直接抄这段代码信号会长期偏多。开 MT5 把 some_threshold 改成你验证集分位数对应的数值再跑。

MQL5 / C++
  if(result != class="num">0)
    {
      Print("Error: ShellExecuteA failed with code ", result);
      class="kw">return;
    }
  class=class="str">"cmt">// Read the prediction from a file
  Sleep(class="num">1000); class=class="str">"cmt">// Wait for the Python script to complete
  fileHandle = FileOpen("/home/int_junkie/Documents/ML/prediction.txt", FILE_READ | FILE_TXT);
  if (fileHandle != INVALID_HANDLE)
    {
      class="type">class="kw">string prediction = FileReadString(fileHandle);
      FileClose(fileHandle);
      class="type">class="kw">double pred_value = StringToDouble(prediction);
      class=class="str">"cmt">// Generate trading signals based on predictions
      class="type">class="kw">double some_threshold = class="num">0.0; class=class="str">"cmt">// Define your threshold
      if (pred_value > some_threshold)
        {
        class=class="str">"cmt">// Buy signal
        class="type">class="kw">double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),Digits());
        class="type">class="kw">double sl = Ask - stopLoss * _Point;
        class="type">class="kw">double tp = Ask + takeProfit * _Point;
        trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, lotsize, Ask, sl, tp, "ML");
        }
      else if (pred_value < some_threshold)
        {
        class=class="str">"cmt">// Sell signal
        class="type">class="kw">double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),Digits());
        class="type">class="kw">double sl = Bid + stopLoss * _Point;
        class="type">class="kw">double tp = Bid - takeProfit * _Point;
        trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, lotsize, Bid, sl, tp, "ML");
        }
    }
  else
    {
      Print("Error: Cannot open prediction file for reading");
    }

「一点提醒」

把 MQL5 和 Jupyter Lab 打通之后,策略研发的路径变了:行情与历史数据先在 MT5 导出,丢进 Jupyter 做统计、可视化与建模,再把训练好的识别逻辑回灌到 MQL5 执行下单。这套链路绕开了 MQL5 原生在高级分析上的短板,附件里的 XAUUSD.m_H1_historical.csv 有约 1.5 MB 的小时级黄金历史,足够本地先跑一遍验证。 值得留意,作者随文给的 jupyterpackage.ipynb 约 323 KB,直接开 MT5 终端 + Jupyter Lab 就能复现流程,不必从零搭环境。但黄金(XAUUSD)与外汇品种杠杆高、跳空频繁,任何由统计模型生成的信号都只是概率倾向,实盘前请用历史 CSV 做样本外回测。 方法本身不保证优势,它只是把「分析—建模—执行」拧成一条可协作的管线;能不能转化成你的边际收益,取决于数据质量和参数纪律。

常见问题

先用探索性数据分析(EDA)跑一遍描述统计和箱线图,重点看偏度、峰度和离群点,再决定后续模型要不要做对数变换或截尾处理。
让 Python 把预测结果写成本地 CSV 或 JSON 文件,实盘端按固定路径定时读取并解析最新一行,避免直接嵌命令行调用导致阻塞。
可以,小布盯盘的 AIGC 已内置多品种诊断,打开对应品种页就能看到数据异常和信号状态,把重复劳动交给它,你专注决策。
用异步文件监听代替同步请求,EA 侧只做信号消费不跑重计算,模型更新间隔设成秒级轮询,降低对报价线程的占用。
注意预测文件时间戳和时区一致性,并对信号做滑点与人手复核,外汇贵金属波动大,外部模型误判可能引发较大回撤。