将 MQL5 与数据处理包集成(第 4 部分):大数据处理·进阶篇
(2/3)· 当历史报价堆到百万行,内置函数开始吃力,这套外挂管线能救回你的回测时间
「在 Jupyter 里给 BTC 一小时数据做清洗与指标落地」
把 MT5 导出的 BTCUSD_H1 原始 CSV 丢进 Jupyter Lab 后,第一件事不是跑模型,而是用 info() 和 head() 确认数据类型、行数和缺失情况。原文数据集经检查后显示:缺失值数量为 0,重复行与重复列均为 0,说明该 H1 切片本身干净,可直接进分析管道。 若源文件实际是制表符分隔(TSV)而非逗号分隔,要在 pd.read_csv() 里显式写 delimiter='\t',否则列会被吞成一整行。解析完后把 <DATE> 与 <TIME> 拼成 DATETIME 并设为索引,再 drop 掉原两列,后续按时间轴画图才不会错位。 技术指标侧,用 rolling(window=20).mean() 算收盘价 20 周期 MA,再用 ta 库的 RSIIndicator(window=14) 补一列 RSI。这两个字段是后面随机森林信号分类的常用特征,也方便手算趋势与超买超卖区间。 可视化用 matplotlib 开 12x6 英寸图,橙线叠 MA20、蓝线画收盘价,肉眼能直接比对价格与均线的穿插关系。外汇与贵金属品种同样适用这套流程,但需注意杠杆品种跳空可能造成缺失值,清洗逻辑要留容错。
class="kw">import pandas as pd # Load the uploaded BTC 1H CSV file file_path = &class="macro">#x27;/home/int_junkie/Documents/DataVisuals/BTCUSD_H1.csv&class="macro">#x27; btc_data = pd.read_csv(file_path) # Display basic information about the dataset btc_data_info = btc_data.info() btc_data_head = btc_data.head() btc_data_info, btc_data_head # Reload the data with tab-separated values btc_data = pd.read_csv(file_path, delimiter=&class="macro">#x27;\t&class="macro">#x27;) # Display basic information and the first few rows after parsing btc_data_info = btc_data.info() btc_data_head = btc_data.head() btc_data_info, btc_data_head # Combine <DATE> and <TIME> into a single class="type">class="kw">datetime column and set it as the index btc_data[&class="macro">#x27;DATETIME&class="macro">#x27;] = pd.to_datetime(btc_data[&class="macro">#x27;<DATE>&class="macro">#x27;] + &class="macro">#x27; &class="macro">#x27; + btc_data[&class="macro">#x27;<TIME>&class="macro">#x27;]) btc_data.set_index(&class="macro">#x27;DATETIME&class="macro">#x27;, inplace=True) # Drop the original <DATE> and <TIME> columns as they&class="macro">#x27;re no longer needed btc_data.drop(columns=[&class="macro">#x27;<DATE>&class="macro">#x27;, &class="macro">#x27;<TIME>&class="macro">#x27;], inplace=True) # Display the first few rows after modifications btc_data.head() # Check for missing values and duplicates missing_values = btc_data.isnull().sum() duplicate_rows = btc_data.duplicated().sum() # Clean data(if needed) btc_data_cleaned = btc_data.drop_duplicates() # Results missing_values, duplicate_rows, btc_data_cleaned.shape # Check for missing values print("Missing values per column:\n", btc_data.isnull().sum()) # Check for duplicate rows print("Number of duplicate rows:", btc_data.duplicated().sum()) # Drop duplicate rows if any btc_data = btc_data.drop_duplicates() # Calculate a class="num">20-period moving average btc_data[&class="macro">#x27;MA20&class="macro">#x27;] = btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;].rolling(window=class="num">20).mean() class="kw">import ta # Add RSI class="kw">using the `ta` library btc_data[&class="macro">#x27;RSI&class="macro">#x27;] = ta.momentum.RSIIndicator(btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;], window=class="num">14).rsi() class="kw">import matplotlib.pyplot as plt # Plot closing price and MA20 plt.figure(figsize=(class="num">12, class="num">6)) plt.plot(btc_data.index, btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;], label=&class="macro">#x27;Close Price&class="macro">#x27;) plt.plot(btc_data.index, btc_data[&class="macro">#x27;MA20&class="macro">#x27;], label=&class="macro">#x27;class="num">20-period MA&class="macro">#x27;, class="type">class="kw">color=&class="macro">#x27;orange&class="macro">#x27;) plt.legend() plt.title(&class="macro">#x27;BTC Closing Price and Moving Average&class="macro">#x27;) plt.show()
◍ 用 Python 把 BTC 小时线切成可回测的特征与信号
这段脚本走的是先清洗、再构造特征、最后用均线交叉出信号的路线,核心在把 MT5 导出的 H1 收盘价转成对数收益并落盘,方便后续反复跑。 日志收益那行用 np.log(close/close.shift(1)),比普通百分比收益更适合后续算相关矩阵;存成 BTCUSD_H1_cleaned.csv 后,seaborn 的 heatmap 能直接把各列相关系数画出来,annot=True 会把数值标在格子里。 特征侧除了 MA20、MA50(窗口 20 / 50 根 H1 K 线),还接了 ta 库的 RSI(14) 与 MACD。注意原文有两套 Signal 逻辑:前一段拿 MA20>RSI 当信号属于写错思路,后一段才是正经 MA20 上穿 MA50 给 1、下穿给 -1,且 shift(1) 避开了未来函数。 train_test_split 按 test_size=0.2、random_state=42 切数据,X 丢掉收盘价、y 用收盘价本身——这其实是回归框架的雏形。Target 列用 shift(-1) 标记下一根是否收高,是典型二分类标签。 回测部分用 pct_change 算市场收益,Signal.shift(1) 乘进去得策略收益,cumprod 画出累计曲线。外汇与贵金属虽不在本例,但同样口径下杠杆品种回测须警惕滑点与隔夜跳空,实际胜率可能明显低于裸曲线。 开 MT5 把 BTCUSD H1 导出来跑一遍这段,重点看 MA20/MA50 交叉信号在 2021 年震荡段的换手频率,能直观判断该策略在无明显趋势时会不会被两面打脸。
class="kw">import numpy as np # Add log returns btc_data[&class="macro">#x27;Log_Returns&class="macro">#x27;] = (btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;] / btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;].shift(class="num">1)).apply(lambda x: np.log(x)) # Save the cleaned data btc_data.to_csv(&class="macro">#x27;BTCUSD_H1_cleaned.csv&class="macro">#x27;) class="kw">import seaborn as sns class="kw">import matplotlib.pyplot as plt # Correlation heatmap sns.heatmap(btc_data.corr(), annot=True, cmap=&class="macro">#x27;coolwarm&class="macro">#x27;) plt.title(&class="macro">#x27;Correlation Heatmap&class="macro">#x27;) plt.show() from sklearn.model_selection class="kw">import train_test_split # Define features and target variable X = btc_data.drop(columns=[&class="macro">#x27;<CLOSE>&class="macro">#x27;]) y = btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;] # Split data X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=class="num">0.2, random_state=class="num">42) # Simple Moving Average Crossover Strategy btc_data[&class="macro">#x27;Signal&class="macro">#x27;] = (btc_data[&class="macro">#x27;MA20&class="macro">#x27;] > btc_data[&class="macro">#x27;RSI&class="macro">#x27;]).astype(class="type">int) btc_data[&class="macro">#x27;Returns&class="macro">#x27;] = btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;].pct_change() btc_data[&class="macro">#x27;Strategy_Returns&class="macro">#x27;] = btc_data[&class="macro">#x27;Signal&class="macro">#x27;].shift(class="num">1) * btc_data[&class="macro">#x27;Returns&class="macro">#x27;] # Plot cumulative returns btc_data[&class="macro">#x27;Cumulative_Strategy&class="macro">#x27;] = (class="num">1 + btc_data[&class="macro">#x27;Strategy_Returns&class="macro">#x27;]).cumprod() btc_data[&class="macro">#x27;Cumulative_Market&class="macro">#x27;] = (class="num">1 + btc_data[&class="macro">#x27;Returns&class="macro">#x27;]).cumprod() btc_data[[&class="macro">#x27;Cumulative_Strategy&class="macro">#x27;, &class="macro">#x27;Cumulative_Market&class="macro">#x27;]].plot(title=&class="macro">#x27;Strategy vs. Market Returns&class="macro">#x27;) plt.show() # Calculate class="type">short-term and class="type">long-term moving averages btc_data[&class="macro">#x27;MA20&class="macro">#x27;] = btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;].rolling(window=class="num">20).mean() btc_data[&class="macro">#x27;MA50&class="macro">#x27;] = btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;].rolling(window=class="num">50).mean() # Generate signals: class="num">1 for Buy, -class="num">1 for Sell btc_data[&class="macro">#x27;Signal&class="macro">#x27;] = class="num">0 btc_data.loc[btc_data[&class="macro">#x27;MA20&class="macro">#x27;] > btc_data[&class="macro">#x27;MA50&class="macro">#x27;], &class="macro">#x27;Signal&class="macro">#x27;] = class="num">1 btc_data.loc[btc_data[&class="macro">#x27;MA20&class="macro">#x27;] < btc_data[&class="macro">#x27;MA50&class="macro">#x27;], &class="macro">#x27;Signal&class="macro">#x27;] = -class="num">1 # Shift signal to avoid look-ahead bias btc_data[&class="macro">#x27;Signal&class="macro">#x27;] = btc_data[&class="macro">#x27;Signal&class="macro">#x27;].shift(class="num">1) # Calculate returns btc_data[&class="macro">#x27;Returns&class="macro">#x27;] = btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;].pct_change() btc_data[&class="macro">#x27;Strategy_Returns&class="macro">#x27;] = btc_data[&class="macro">#x27;Signal&class="macro">#x27;] * btc_data[&class="macro">#x27;Returns&class="macro">#x27;] # Calculate cumulative returns btc_data[&class="macro">#x27;Cumulative_Market&class="macro">#x27;] = (class="num">1 + btc_data[&class="macro">#x27;Returns&class="macro">#x27;]).cumprod() btc_data[&class="macro">#x27;Cumulative_Strategy&class="macro">#x27;] = (class="num">1 + btc_data[&class="macro">#x27;Strategy_Returns&class="macro">#x27;]).cumprod() # Plot performance class="kw">import matplotlib.pyplot as plt plt.figure(figsize=(class="num">12, class="num">6)) plt.plot(btc_data[&class="macro">#x27;Cumulative_Market&class="macro">#x27;], label=&class="macro">#x27;Market Returns&class="macro">#x27;) plt.plot(btc_data[&class="macro">#x27;Cumulative_Strategy&class="macro">#x27;], label=&class="macro">#x27;Strategy Returns&class="macro">#x27;) plt.title(&class="macro">#x27;Strategy vs. Market Performance&class="macro">#x27;) plt.legend() plt.show() # Add RSI from ta.momentum class="kw">import RSIIndicator btc_data[&class="macro">#x27;RSI&class="macro">#x27;] = RSIIndicator(btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;], window=class="num">14).rsi() # Add MACD from ta.trend class="kw">import MACD macd = MACD(btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;]) btc_data[&class="macro">#x27;MACD&class="macro">#x27;] = macd.macd() btc_data[&class="macro">#x27;MACD_Signal&class="macro">#x27;] = macd.macd_signal() # Target variable: class="num">1 if next period&class="macro">#x27;s close > current close btc_data[&class="macro">#x27;Target&class="macro">#x27;] = (btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;].shift(-class="num">1) > btc_data[&class="macro">#x27;<CLOSE>&class="macro">#x27;]).astype(class="type">int) from sklearn.model_selection class="kw">import train_test_split
用随机森林给 BTC 方向分类跑一遍
下面这段 Python 代码把 BTC 的均线、RSI 和 MACD 类因子塞进随机森林,做二分类预测,能直接复用到 MT5 导出的 CSV 上做离线验证。 特征取了 MA20、MA50、RSI、MACD、MACD_Signal 五个字段,目标列是预先打好的 Target(涨/跌标签)。用 train_test_split 按 0.2 比例切测试集,随机种子锁 42,保证每次跑结果可复现。 模型用 RandomForestClassifier,n_estimators=100,训练完在测试集上 print 出 accuracy 和 classification_report,能直接看到各类精确率、召回率。 最后用 joblib.dump 把模型存成 btc_trading_model.pkl,之后小布类工具加载这个文件就能对新的 MT5 日线快照做批量推理。外汇与贵金属波动更剧烈,直接套用 crypto 训练集概率会漂移,需重采样本地品种数据。
from sklearn.ensemble class="kw">import RandomForestClassifier from sklearn.metrics class="kw">import accuracy_score, classification_report # Define features and target features = [&class="macro">#x27;MA20&class="macro">#x27;, &class="macro">#x27;MA50&class="macro">#x27;, &class="macro">#x27;RSI&class="macro">#x27;, &class="macro">#x27;MACD&class="macro">#x27;, &class="macro">#x27;MACD_Signal&class="macro">#x27;] X = btc_data.dropna()[features] y = btc_data.dropna()[&class="macro">#x27;Target&class="macro">#x27;] # Split data X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=class="num">0.2, random_state=class="num">42) # Train a Random Forest Classifier model = RandomForestClassifier(n_estimators=class="num">100, random_state=class="num">42) model.fit(X_train, y_train) # Evaluate the model y_pred = model.predict(X_test) print("Accuracy:", accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred)) class="kw">import joblib # Save the model joblib.dump(model, &class="macro">#x27;btc_trading_model.pkl&class="macro">#x27;)
「用 WebRequest 把 MT5 接到 Python 模型」
想在 MT5 里跑训练好的模型,核心不是重写算法,而是打通 MQL5 与 Python 的通信。最轻量的办法是用 WebRequest 走本地 HTTP:MQL5 负责算指标、组特征,Python 端监听 127.0.0.1:5000/predict 做推理,返回买卖信号。 GetPrediction() 先把 double 型特征数组拼成 JSON 串,DoubleToString 锁死六位小数,再转 char 数组发 POST。若 WebRequest 返回 -1,说明请求没发出去,函数也回 -1 给上层,避免假信号。 OnTick 里先抓 MA20、MA50、RSI14 以及 MACD 三线(线/信号/柱),塞进 features 数组送模型。模型回 1 倾向买、回 -1 倾向卖,实盘用 MBuy / MSell 跟单。外汇与贵金属杠杆高,信号只是概率倾向,开 MT5 前先确认 Python 端已起服务,否则全部请求会落空。 下面这段是通信函数的骨架,逐行看能直接抄去改端点: // 头两行声明版权与链接属性,可忽略 // #property copyright / link / version 是脚本元信息 // #include <Trade\Trade.mqh> 引入交易库,CTrade trade 实例化下单对象 // GetPrediction 入参是 double 引用数组 features[] // 循环里 DoubleToString(features[i],6) 保证每值六位小数,逗号分隔拼成 [..] // url 写死本地 5000 端口 /predict // StringToCharArray 把 json 请求转 char 待发 // WebRequest 用 POST,超时 500 毫秒,响应进 resultsss // 若 result==-1 打错误码并 return -1 // CharArrayToString 把响应还原字符串,StringToDouble 取预测值
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| BTC-Big-DataH.mq5 | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Trade\Trade.mqh> CTrade trade; class=class="str">"cmt">// Function to get predictions from Python API class="type">class="kw">double GetPrediction(class="type">class="kw">double &features[]) { class=class="str">"cmt">// Convert the features array to a JSON-like class="type">class="kw">string class="type">class="kw">string jsonRequest = "["; for (class="type">int i = class="num">0; i < ArraySize(features); i++) { jsonRequest += DoubleToString(features[i], class="num">6); if (i != ArraySize(features) - class="num">1) jsonRequest += ","; } jsonRequest += "]"; class=class="str">"cmt">// Define the WebRequest parameters class="type">class="kw">string url = "http:class=class="str">"cmt">//class="num">127.0.class="num">0.1:class="num">5000/predict"; class="type">class="kw">string hdrs = {"Content-Type: application/json"}; class=class="str">"cmt">// Add headers if needed class="type">char data[]; StringToCharArray(jsonRequest, data); class=class="str">"cmt">// Convert JSON request class="type">class="kw">string to class="type">char array class="type">char response[]; class="type">ulong result_headers_size = class="num">0; class=class="str">"cmt">//-------------------------------------------------------------------------------------- class="type">class="kw">string cookie=NULL; class="type">char post[], resultsss[]; class=class="str">"cmt">// Send the WebRequest class="type">int result = WebRequest("POST", url, cookie, NULL, class="num">500, post, class="num">0, resultsss, hdrs); class=class="str">"cmt">// Handle the response if (result == -class="num">1) { Print("Error sending WebRequest: ", GetLastError()); class="kw">return -class="num">1; class=class="str">"cmt">// Return an error signal } class=class="str">"cmt">// Convert response class="type">char array back to a class="type">class="kw">string class="type">class="kw">string responseString; CharArrayToString(response, (class="type">int)responseString); class=class="str">"cmt">// Parse the response(assuming the server returns a numeric value) class="type">class="kw">double prediction = StringToDouble(responseString); class="kw">return prediction; }