将 MQL5 与数据处理包集成(第 1 部分):高级数据分析和统计处理·进阶篇
用 EDA 摸清 XAUUSD 数据的底
探索性数据分析(EDA)不是为了炫图,而是先搞清楚手里的历史报价到底长什么样。对黄金小时线这类高频序列,先确认缺失值、异常跳空和列类型,再做分布观察,否则后面训练的模型只会学出一地鸡毛。 下面这段 Python 脚本直接读入 XAUUSD 的 H1 历史 csv,先打印 info 和 describe 看字段概况,再画收盘价走势,最后用 TA-Lib 算 14 周期 RSI 做特征。注意它只拿 RSI 单列去预测下一根收盘价,属于刻意压到最简化来验证基线。 相关矩阵是 EDA 里不能跳的一步:接近 1 是同向强相关(如开盘与收盘),接近 -1 是反向强相关(如放量与价跌的概率倾向),接近 0 则基本无线性关联。黄金短线里 RSI 与隔根收盘往往相关系数偏低,单特征模型容易扑街。 回测现象很直白——预测值与实际值明显错位,MSE 打印出来通常偏大。常见病因就四个:样本太小、过拟合、欠拟合、特征工程太糙。诊断时优先查数据干净度,再用 MSE / MAE 量化,别凭眼睛猜。 外汇与贵金属杠杆高、滑点凶,这类实验只在 MT5 导出的历史数据上做离线验证,任何‘模型能预测’都只是概率倾向,实盘前必须重采样交叉核对。
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: sns.histplot(data=data, x=i) 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 # 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;<CLOSE>&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;<DATE>&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;<CLOSE>&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;<CLOSE>&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>]] # 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/管道捕获的字符串,外汇与贵金属杠杆高、模型错判会放大亏损,实盘前务必在策略测试器里先验证通路。 下面这段是可直接抄去改路径的原型,路径写死仅为示例,真实部署建议仍走参数传入。
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 带着脏信号下单。 阈值怎么切信号很直接:预测值大于阈值倾向出买讯,小于阈值倾向出卖讯。外汇和贵金属杠杆高、滑点跳空频繁,这套桥接方案只是把模型推断搬进终端,实盘胜率仍受样本外漂移影响,参数和阈值都得自己回测。
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 改成你验证集分位数对应的数值再跑。
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 做样本外回测。 方法本身不保证优势,它只是把「分析—建模—执行」拧成一条可协作的管线;能不能转化成你的边际收益,取决于数据质量和参数纪律。