利用 Python 和 MQL5 构建您的第一个玻璃盒模型(基础篇)
📘

利用 Python 和 MQL5 构建您的第一个玻璃盒模型(基础篇)

第 1/3 篇

「用 Python 搭一个看得透的 MT5 模型」

很多 EA 本质是黑盒:参数一多,你根本说不清盈利到底来自哪条规则。玻璃盒模型反过来,把每步决策暴露给人工检查,适合想搞明白「为什么下单」的贵金属与外汇交易者。 MT5 原生 MQL5 擅长执行与指标,但统计建模和特征工程用 Python 更顺手。两者通过零延迟网桥或文件管道打通后,你可以在 Python 里训练逻辑、在 MQL5 里落地下单。 外汇与贵金属杠杆高、滑点突发行情多,这类跨语言方案只降低逻辑黑度,不降低爆仓风险,先用模拟盘验证再谈真金。

透明模型怎么接进 MT5

玻璃盒算法属于机器学习里的一类,特点是内部逻辑完全可见、可被人读懂。它打破了「准确率与可解释性只能二选一」的旧认知——在保持高精度的同时,迭代时更容易排错、维护和改进。相对地,黑盒模型虽然能拟合高维非线性关系,但内部机制对人类不友好,调试成本偏高。 经验上的取舍很直接:只有当玻璃盒模型达不到所需精度时,才该上黑盒。本文要落地的,是一个玻璃盒模型接进 MetaTrader 5 的两种路径。外汇与贵金属杠杆高、滑点跳空频繁,模型再透明也替代不了仓位风控。 传统接法是走 MT5 自带的 Python 库,把玻璃盒模型连到终端,再用 MQL5 写个 EA 做辅助执行,门槛最低。当下更推荐的是把模型导出成 ONNX(开放神经网络交换格式),作为资源直接编进 EA,这样能吃掉 MT5 全部原生接口,玻璃盒的算力不必绕路。

◍ 黑盒与玻璃盒:可解释性才是真门槛

传统机器学习里,多数模型内部逻辑复杂到无法直观解释,这类被统称为黑盒模型。黑盒的问题不在于预测,而在于你没法靠它去改进关键性能度量——你只知道结果,不知道为什么。 玻璃盒模型则反过来,内部工作机制透明可理解,同时预测精度并不输黑盒。微软维护的 Interpret ML(Python 包)同时收了黑盒解释器和玻璃盒模型两类工具;其中黑盒解释器多为模型无关算法,能套在任何黑盒上做评估,但它只给评估、不给你因果,这正是后续要拆的坑。 对做外汇或贵金属量化的人来说,模型不可解释等于没法做风控复盘,高杠杆下这是实打实的高风险。玻璃盒能让你看见特征怎么影响输出,比盲目信回测曲线靠谱得多。 本文接下来会用 Interpret ML 在 python 里搭一个玻璃盒模型,直接看它如何暴露关键特征参照,帮你把模型内部逻辑摸透。

「解释器互相打架时模型还能信吗」

用黑盒模型做行情分类或择时,最烦的不是精度不够,而是同一个模型换套解释方法会给出完全不同的特征重要性。互信息(mutual information)和排列重要性(permutation importance)都声称在拆黑盒,但前者看变量与目标的信息重叠,后者看打乱后分数掉多少,关注的侧面不同,推出来的底层度量可能直接矛盾。 这种分歧不是偶发噪声,而是开放研究课题。交易者若只信一种解释器挑出来的信号,容易把模型偏见当成市场规律。后面直接用 MT5 实盘数据演示:同一组 Boom 1000 Index 日线,两种解释技术跑出来谁主导模型,肉眼就能比出来。 下面这段 Python 接 MT5 终端拉数据的代码,是后面做分歧对比的底座。注意 login、password、server 要填你自己的 Deriv-Demo 或实盘环境,copy_rates_range 这里抓的是 2019-04-17 到当前的 D1 数据,换成 Volatility 75 也行。 代码逐行拆解:import MetaTrader5 as mt5 是接终端的核心包;datetime 用来框日期;matplotlib 和 pandas 管画图与表格;pandas_ta 算技术指标;precision_score 评分类精度;mutual_info_classif 与 permutation_importance 就是两套会打架的解释器;XGBClassifier 是黑盒本体。mt5.initialize 带账号密码一键登录,symbols_get 打印所有可交易品种名,copy_rates_range 拉完存进 DataFrame 并把秒级 time 转成日期。

MQL5 / C++
class="macro">#Import MetaTrader5 Python package
class="macro">#pip install --upgrade MetaTrader5, if you don&class="macro">#x27;t have it installed
class="kw">import MetaTrader5 as mt5
class="macro">#Import class="type">class="kw">datetime for selecting data
class="macro">#Standard python package, no installation required
from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime
class="macro">#Plotting Data
class="macro">#pip install --upgrade matplotlib, if you don&class="macro">#x27;t have it installed
class="kw">import matplotlib.pyplot as plt
class="macro">#Import pandas for handling data
class="macro">#pip install --upgrade pandas, if you don&class="macro">#x27;t have it installed
class="kw">import pandas as pd
class="macro">#Import library for calculating technical indicators
class="macro">#pip install --upgrade pandas-ta, if you don&class="macro">#x27;t have it installed
class="kw">import pandas_ta as ta
class="macro">#Scoring metric to assess model accuracy
class="macro">#pip install --upgrade scikit-learn, if you don&class="macro">#x27;t have it installed
from sklearn.metrics class="kw">import precision_score
class="macro">#Import mutual information, a black-box explanation technique
from sklearn.feature_selection class="kw">import mutual_info_classif
class="macro">#Import permutation importance, another black-box explanation technique
from sklearn.inspection class="kw">import permutation_importance
class="macro">#Import our model
class="macro">#pip install --upgrade xgboost, if you don&class="macro">#x27;t have it installed
from xgboost class="kw">import XGBClassifier
class="macro">#Plotting model importance
from xgboost class="kw">import plot_importance
class="macro">#Enter your account number
login = class="num">123456789
class="macro">#Enter your password
password = &class="macro">#x27;_enter_your_password_&class="macro">#x27;
class="macro">#Enter your Broker&class="macro">#x27;s server
server = &class="macro">#x27;Deriv-Demo&class="macro">#x27;
class="macro">#We can initialize the MT5 terminal and login to our account in the same step
if mt5.initialize(login=login,password=password,server=server):
    print(&class="macro">#x27;Logged in successfully&class="macro">#x27;)
else:
    print(&class="macro">#x27;Failed To Log in&class="macro">#x27;)
class="macro">#To view all available symbols from your broker
symbols = mt5.symbols_get()
for index,value in enumerate(symbols):
    print(value.name)
class="macro">#We need to specify the dates we want to use in our dataset
date_from = class="type">class="kw">datetime(class="num">2019,class="num">4,class="num">17)
date_to = class="type">class="kw">datetime.now()
class="macro">#Fetching historical data
data = pd.DataFrame(mt5.copy_rates_range(&class="macro">#x27;Boom class="num">1000 Index&class="macro">#x27;,mt5.TIMEFRAME_D1,date_from,date_to))
class="macro">#Let&class="macro">#x27;s convert the time from seconds to year-month-date
data[&class="macro">#x27;time&class="macro">#x27;] = pd.to_datetime(data[&class="macro">#x27;time&class="macro">#x27;],unit=&class="macro">#x27;s&class="macro">#x27;)
data

用预处理函数给行情帧灌入技术指标

前一节我们看到导出的数据帧里时间已可读,但 real_volume 整列都是 0,这种无效字段直接丢掉能省掉后续计算的噪音。 真正有用的活儿是批量塞指标。下面这段 Python 函数一次完成 ATR(14)、RSI(14) 及其滚动标准差的注入,顺便算出高低中点、与前一日中点的偏移、以及收盘价离中点的距离,最后清掉带空值的行。 [CODE] #Let's create a function to preprocess our data def preprocess(df): #All values of real_volume are 0 in this dataset, we can drop the column df.drop(columns={'real_volume'},inplace=True) #Calculating 14 period ATR df.ta.atr(length=14,append=True) #Calculating the growth in the value of the ATR, the second difference df['ATR Growth'] = df['ATRr_14'].diff().diff() #Calculating 14 period RSI df.ta.rsi(length=14,append=True) #Calculating the rolling standard deviation of the RSI df['RSI Stdv'] = df['RSI_14'].rolling(window=14).std() #Calculating the mid point of the high and low price df['mid_point'] = ( ( df['high'] + df['low'] ) / 2 ) #We will keep track of the midpoint value of the previous day df['mid_point - 1'] = df['mid_point'].shift(1) #How far is our price from the midpoint? df['height'] = df['close'] - df['mid_point'] #Drop any rows that have missing values df.dropna(axis=0,inplace=True) [/CODE] 逐行看:第 4 行原地删掉 real_volume 列;第 6 行用 ta.atr 算 14 周期 ATR 并追加为 ATRr_14;第 8 行对 ATRr_14 做两次差分得到 ATR Growth,能反映波动率加速度。 第 10–11 行追加 RSI_14,第 13 行用 14 窗口滚动标准差量化 RSI 的离散程度;第 15–19 行构造 mid_point 及滞后一期值,height 则是收官价相对中点的偏离。最后 dropna 保证进模型的数据无缺口。 在 MT5 里导出 EURUSD 日线跑一遍,你会看到前 27 行因 ATR/RSI 窗口与差分被自然剔除,这是 14 周期类指标固定的预热损耗,属正常现象。外汇与贵金属杠杆高、滑点大,上述特征仅作概率参考,实盘须自担风险。

MQL5 / C++
class="macro">#Let&class="macro">#x27;s create a function to preprocess our data
def preprocess(df):
    class="macro">#All values of real_volume are class="num">0 in this dataset, we can drop the column
    df.drop(columns={&class="macro">#x27;real_volume&class="macro">#x27;},inplace=True) 
    class="macro">#Calculating class="num">14 period ATR
    df.ta.atr(length=class="num">14,append=True)
    class="macro">#Calculating the growth in the value of the ATR, the second difference
    df[&class="macro">#x27;ATR Growth&class="macro">#x27;] = df[&class="macro">#x27;ATRr_14&class="macro">#x27;].diff().diff()
    class="macro">#Calculating class="num">14 period RSI
    df.ta.rsi(length=class="num">14,append=True)    
    class="macro">#Calculating the rolling standard deviation of the RSI
    df[&class="macro">#x27;RSI Stdv&class="macro">#x27;] = df[&class="macro">#x27;RSI_14&class="macro">#x27;].rolling(window=class="num">14).std()
    class="macro">#Calculating the mid point of the high and low price
    df[&class="macro">#x27;mid_point&class="macro">#x27;] = ( ( df[&class="macro">#x27;high&class="macro">#x27;] + df[&class="macro">#x27;low&class="macro">#x27;] ) / class="num">2 ) 
    class="macro">#We will keep track of the midpoint value of the previous day
    df[&class="macro">#x27;mid_point - class="num">1&class="macro">#x27;] = df[&class="macro">#x27;mid_point&class="macro">#x27;].shift(class="num">1) 
    class="macro">#How far is our price from the midpoint?
    df[&class="macro">#x27;height&class="macro">#x27;] = df[&class="macro">#x27;close&class="macro">#x27;] - df[&class="macro">#x27;mid_point&class="macro">#x27;] 
    class="macro">#Drop any rows that have missing values
    df.dropna(axis=class="num">0,inplace=True)

◍ 解释器互相打架时该信谁

把数据帧跑完预处理后,目标被定义成「次日收盘是否高于今日收盘」的 0/1 哑变量。按时间序列切分(train_start=27 到 train_end=1000,test_start=1001 起),XGBClassifier 在测试集上的 precision_score 只有 0.459,也就是约 46% 的命中率,黑盒本身并不神。 XGBoost 自带的 plot_importance 能直接看特征权重,但神经网络、SVM 没有这种内建函数,只能自己反推权重。更麻烦的是,换用排列重要性(permutation_importance)洗牌各列算损失变化,排第一的居然是 ATR 读数;而我们从构造逻辑知道真实第六才轮到 ATR,真正关键是 ATR Growth。互通信息(mutual_info_classif)跑出来更是近乎逆序:RSI_14 得 0.0146 最高,open 0.0100 次之,ATR Growth 和 height 直接是 0.0000。 三种解释器给的重要性排名互不相同,如果你手里没有「基本事实」,很容易挑一个符合自己直觉的排名——这正好掉进确认偏见的坑。外汇与贵金属行情具有高杠杆与跳空风险,这类模型解释结论只可作辅助,不能直接当方向依据。 实盘前建议你在 MT5 导出的 tick 数据上复跑这段 Python,把 predictors 里的 ATR Growth 删掉再 fit 一次,看 precision 是否真的掉点,比盲信任何一张 importance 图都实在。

MQL5 / C++
preprocess(data)
data
class="macro">#We want to predict whether tomorrow&class="macro">#x27;s close will be greater than today&class="macro">#x27;s close
class="macro">#We can encode a dummy variable for that: 
#class="num">1 means tomorrow&class="macro">#x27;s close will be greater.
#class="num">0 means today&class="macro">#x27;s close will be greater than tomorrow&class="macro">#x27;s.
data[&class="macro">#x27;target&class="macro">#x27;] = (data[&class="macro">#x27;close&class="macro">#x27;].shift(-class="num">1) > data[&class="macro">#x27;close&class="macro">#x27;]).astype(class="type">int)
data
class="macro">#The first date is class="num">2019-class="num">05-class="num">14, and the first close price is class="num">9029.486, the close on the next day class="num">2019-class="num">05-class="num">15 was class="num">8944.461
class="macro">#So therefore, on the first day, class="num">2019-class="num">05-class="num">14, the correct forecast is class="num">0 because the close price fell the following day.
class="macro">#Seperating predictors and target
predictors = [&class="macro">#x27;open&class="macro">#x27;,&class="macro">#x27;high&class="macro">#x27;,&class="macro">#x27;low&class="macro">#x27;,&class="macro">#x27;close&class="macro">#x27;,&class="macro">#x27;tick_volume&class="macro">#x27;,&class="macro">#x27;spread&class="macro">#x27;,&class="macro">#x27;ATRr_14&class="macro">#x27;,&class="macro">#x27;ATR Growth&class="macro">#x27;,&class="macro">#x27;RSI_14&class="macro">#x27;,&class="macro">#x27;RSI Stdv&class="macro">#x27;,&class="macro">#x27;mid_point&class="macro">#x27;,&class="macro">#x27;mid_point - class="num">1&class="macro">#x27;,&class="macro">#x27;height&class="macro">#x27;]
target     = [&class="macro">#x27;target&class="macro">#x27;]
class="macro">#The training and testing split definition
train_start = class="num">27
train_end = class="num">1000
test_start = class="num">1001
class="macro">#Train set
train_x = data.loc[train_start:train_end,predictors]
train_y = data.loc[train_start:train_end,target]
class="macro">#Test set
test_x = data.loc[test_start:,predictors]
test_y = data.loc[test_start:,target]
class="macro">#Let us fit our model
black_box = XGBClassifier()
black_box.fit(train_x,train_y)
class="macro">#Let&class="macro">#x27;s see our model predictions
black_box_predictions = pd.DataFrame(black_box.predict(test_x),index=test_x.index)
class="macro">#Assesing model prediction accuracy
black_box_score = precision_score(test_y,black_box_predictions)
class="macro">#Model precision score
black_box_score
plot_importance(black_box)
class="macro">#Now let us observe the disagreement problem
black_box_pi = permutation_importance(black_box,train_x,train_y)
# Get feature importances and standard deviations
perm_importances = black_box_pi.importances_mean
perm_std = black_box_pi.importances_std
# Sort features based on importance
sorted_idx = perm_importances.argsort()
class="macro">#We&class="macro">#x27;re going to utilize a bar histogram
plt.barh(range(train_x.shape[class="num">1]), perm_importances[sorted_idx], xerr=perm_std[sorted_idx])
plt.yticks(range(train_x.shape[class="num">1]), train_x.columns[sorted_idx])
plt.xlabel(&class="macro">#x27;Permutation Importance&class="macro">#x27;)
plt.title(&class="macro">#x27;Permutation Importances&class="macro">#x27;)
plt.show()
class="macro">#Let&class="macro">#x27;s see if our black-box explainers will disagree with each other by calculating mutual information
black_box_mi = mutual_info_classif(train_x,train_y)
black_box_mi = pd.Series(black_box_mi, name="MI Scores", index=train_x.columns)
black_box_mi = black_box_mi.sort_values(ascending=False)
black_box_mi

常见问题

在 Python 侧用预处理函数把行情帧灌入技术指标,生成特征后通过本地 socket 或文件桥传给 MT5 的 EA 读取并执行,避免直接黑盒调用。
可解释性才是真门槛,玻璃盒能看清每个特征如何影响决策,黑盒出错了你连改都没法改,外汇贵金属高杠杆下风险会被放大。
小布可以接入你的品种页,把模型输出的特征权重和当前行情叠加展示,帮你快速判断信号逻辑是否自洽,不用自己挨个翻解释器。
先看哪个解释器覆盖的特征与你预处理逻辑一致,再回测该解释器在近期行情的胜率,倾向信稳定性更高的那一个,别盲信单一视图。
容易漏掉不同周期对齐和缺失值处理,建议在预处理函数里显式做时间戳重采样与向前填充,否则特征错位会让透明模型也给出歪信号。