利用 Python 和 MQL5 构建您的第一个玻璃盒模型·进阶篇
📘

利用 Python 和 MQL5 构建您的第一个玻璃盒模型·进阶篇

第 2/3 篇

「为什么交易系统该用玻璃盒」

玻璃盒模型把决策逻辑完全摊开,不像黑盒那样只能看输入输出。对做价格行为分析的人而言,能直接读懂模型在哪根 K 线、哪个阈值上触发信号,比事后拿 SHAP 值反推黑盒省事得多。 调试同样是硬优势:当模型灵活性和黑盒相当时,玻璃盒的定位错误时间成本明显更低。我们实测中改一处特征权重就能在 MT5 里复跑验证,而同复杂度黑盒往往要重训加对比,耗时差出数倍。 精度并不会因为透明而打折——经验法则是,只有玻璃盒实在达不到同等精度时才退而用黑盒。外汇与贵金属波动受宏观事件扰动大,高风险环境下可解释性直接决定你敢不敢跟单。 本篇后续会落地第一个玻璃盒模型,测性能、提准确率,再接 MT5 终端实跑,顺带写个 EA 辅助调用,最后转 ONNX 释放两端潜力。

把 MT5 数据喂给可解释助推分类器

登录、拉历史、算指标、打标签这几步和黑盒脚本完全一致,这里只拆玻璃盒独有的链路。先装 interpret 包,再载入 MT5、pandas_ta 与 sklearn 的精度函数,核心是从 interpret.glassbox 引入 ExplainableBoostingClassifier——它本身就是白盒,不需要额外反演工具。 训练测试拆分必须按时间顺序切,不能随机洗牌。随机拆分会让回测对未来表现画出过于乐观的假象,这点在外汇与贵金属这种高波动品种上尤其容易误导。 拟合后用 explain_global() 看全局状态:模型把滞后中点值排为最重要特征,其次捕捉到 ATR 增幅与滞后中点的交汇项,高度及收盘价-高度交汇项紧随。这些信息直接指明后续该换哪些参照特征,没有黑盒的分歧空间。 精度实测只有 0.49095022624434387,约 49%;ROC 曲线的 AUC 同样为 0.49。相比前篇 XGBClassifier 的黑盒,可解释助推在牺牲少量准确率(概率倾向)的前提下换来了逐特征可读。 用 explain_local() 能翻每一次预测的局部解释:某根 K 线实际跌(类 0)却被估成涨(类 1),模型给错分的概率 53%,其中 RSI 偏蓝(反向拖累),而价差-高度交汇项为橙(指向正确)。这类局部贡献在外汇高风险环境下值得单根复核,别直接当信号。

MQL5 / C++
class="macro">#Installing Interpret ML
pip install --upgrade interpret
<span class="preprocessor">class="macro">#Import </span>MetaTrader5 package
class="kw">import MetaTrader5 <span class="keyword">as</span> mt5
class="macro">#Import class="type">class="kw">datetime for selecting data
from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime
class="macro">#Import matplotlib for plotting
class="kw">import matplotlib.pyplot as plt
<span class="preprocessor">class="macro">#Intepret </span>glass-box model <span class="keyword">for</span> classification
from interpret.glassbox class="kw">import ExplainableBoostingClassifier
<span class="preprocessor">class="macro">#Intepret </span>GUI dashboard utility
from interpret class="kw">import show
class="macro">#Visualising our model&class="macro">#x27;s performance in one graph
from interpret.perf class="kw">import ROC
<span class="preprocessor">class="macro">#Pandas </span><span class="keyword">for</span> handling data
class="kw">import pandas <span class="keyword">as</span> pd
<span class="preprocessor">class="macro">#Pandas-ta </span><span class="keyword">for</span> calculating technical indicators
class="kw">import pandas_ta <span class="keyword">as</span> ta
<span class="preprocessor">class="macro">#Scoring </span>metric to assess model accuracy
from sklearn.metrics class="kw">import precision_score
<span class="preprocessor">class="macro">#Let </span>us fit our glass-box model
<span class="preprocessor">class="macro">#Please </span>note <span class="keyword">this</span> step can take a <span class="keyword">class="kw">while</span>, depending on your computational resources
glass_box = ExplainableBoostingClassifier()
glass_box.fit(train_x,train_y)
class="macro">#The show function provides an interactive GUI dashboard <span class="keyword">for</span> us to <span class="keyword">interface</span> with <span class="keyword">out</span> model
class="macro">#The explain_global() function helps us find what our model found important and allows us to identify potential bias or unintended flaws
show(glass_box.explain_global())
<span class="preprocessor">class="macro">#Obtaining </span>glass-box predictions
glass_box_predictions = pd.DataFrame(glass_box.predict(test_x))
glass_box_score = precision_score(test_y,glass_box_predictions)
glass_box_score
<span class="preprocessor">class="macro">#We </span>can also obtain individual explanations <span class="keyword">for</span> each prediction
show(glass_box.explain_local(test_x,test_y))
glass_box_performance = ROC(glass_box.predict_proba).explain_perf(test_x,test_y, name=&class="macro">#x27;Glass Box&class="macro">#x27;)
show(glass_box_performance)

◍ 把玻璃盒预测塞进 MT5 实盘通道

橡胶落到路面这一刻,先把玻璃盒模型接到 MT5 终端。最省事的做法是从账户状态和品种列表摸起:账户余额 912.11、净值 912.11,说明当前无浮盈浮亏敞口;遍历 broker 品种后,Boom 1000 Index 的最小交易量报 0.2,这一步别写死仓位,直接动态抓取 volume_min 再乘系数,能避掉无效订单退回。 我们不在代码里硬编码手数,而是设 VOLUME=0 再由循环里 symbol.volume_min * LOT_MULTIPLE 赋值,LOT_MULTIPLE 设为 1 就是每笔以最小量进场。DEVIATION=100 给成交价留了缓冲,TIMEFRAME 锁日线,这些全局变量决定了机器人后面怎么跑。 下单与平仓各写一个辅助函数。market_order 用 symbol_info_tick 抓 bid/ask,买挂 ask、卖挂 bid,action 走 TRADE_ACTION_DEAL,填 magic=100 方便日后在终端筛自选单;close_order 靠 ticket 反查持仓,把持仓 type 反转(0 买变 1 卖)来平。实测一次 sell 信号下,OrderSendResult 回 retcode=10009,成交 0.2 手、price=16042.867,说明通道通了。 外汇与合成指数品种杠杆高、滑点跳空频繁,最小交易量随经纪商变,真跑之前先在策略测试器用演示账户验一遍返回结构再接实盘。

MQL5 / C++
class="macro">#Fetching account Info
account_info = mt5.account_info()
# getting specific account data
initial_balance = account_info.balance
initial_equity = account_info.equity
print(&class="macro">#x27;balance: &class="macro">#x27;, initial_balance)
print(&class="macro">#x27;equity: &class="macro">#x27;, initial_equity)
symbols = mt5.symbols_get()
class="macro">#Trading global variables
class="macro">#The symbol we want to trade
MARKET_SYMBOL = &class="macro">#x27;Boom class="num">1000 Index&class="macro">#x27;
class="macro">#This data frame will store the most recent price update
last_close = pd.DataFrame()
class="macro">#We may not always enter at the price we want, how much deviation can we tolerate?
DEVIATION = class="num">100
class="macro">#For demonstrational purposes we will always enter at the minimum volume
class="macro">#However,we will not hardcode the minimum volume, we will fetch it dynamically
VOLUME = class="num">0
class="macro">#How many times the minimum volume should our positions be
LOT_MUTLIPLE = class="num">1
class="macro">#What timeframe are we working on?
TIMEFRAME = mt5.TIMEFRAME_D1
for index,symbol in enumerate(symbols):
    if symbol.name == MARKET_SYMBOL:
        print(f"{symbol.name} has minimum volume: {symbol.volume_min}")
        VOLUME = symbol.volume_min * LOT_MULTIPLE
# function to send a market order
def market_order(symbol, volume, order_type, **kwargs):
    class="macro">#Fetching the current bid and ask prices
    tick = mt5.symbol_info_tick(symbol)
    
    class="macro">#Creating a dictionary to keep track of order direction
    order_dict = {&class="macro">#x27;buy&class="macro">#x27;: class="num">0, &class="macro">#x27;sell&class="macro">#x27;: class="num">1}
    price_dict = {&class="macro">#x27;buy&class="macro">#x27;: tick.ask, &class="macro">#x27;sell&class="macro">#x27;: tick.bid}
    request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": symbol,
        "volume": volume,
        "type": order_dict[order_type],
        "price": price_dict[order_type],
        "deviation": DEVIATION,
        "magic": class="num">100,
        "comment": "Glass Box Market Order",
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_FOK,
    }
    order_result = mt5.order_send(request)
    print(order_result)
    class="kw">return order_result
# Closing our order based on ticket id
def close_order(ticket):
    positions = mt5.positions_get()
    for pos in positions:
        tick = mt5.symbol_info_tick(pos.symbol) class="macro">#validating that the order is for this symbol
        type_dict = {class="num">0: class="num">1, class="num">1: class="num">0}  # class="num">0 represents buy, class="num">1 represents sell - inverting order_type to close the position
        price_dict = {class="num">0: tick.ask, class="num">1: tick.bid} class="macro">#bid ask prices
        if pos.ticket == ticket:
            request = {
                "action": mt5.TRADE_ACTION_DEAL,
                "position": pos.ticket,
                "symbol": pos.symbol,
                "volume": pos.volume,
                "type": type_dict[pos.type],
                "price": price_dict[pos.type],
                "deviation": DEVIATION,
                "magic": class="num">100,

「用 Python 把玻璃盒信号接进 MT5 下单循环」

这段脚本把前面训练好的玻璃盒模型接到了 MT5 实盘接口上。核心是一个 ai_signal() 函数:从 2023-11-01 拉到当前时间的 OHLC,用下一根 close 是否大于当前 close 打标签,取最后一行喂给模型,返回 1(偏多)或 0(偏空)。 主循环里 signal==1 译作 buy、signal==0 译作 sell,先扫一遍持仓把反向单平掉再开仓。注意原文里 mt5.positions_totoal() 是拼写错误,真跑会直接抛 AttributeError,得改成 mt5.positions_total()。 外汇与贵金属杠杆高、滑点跳空频繁,FOK 填充策略在流动性差时可能直接拒单,实盘前建议在策略测试器或模拟账户验证至少 200 根 K 线。

MQL5 / C++
request["comment"] = "Glass Box Close Order"
request["type_time"] = mt5.ORDER_TIME_GTC
request["type_filling"] = mt5.ORDER_FILLING_FOK
order_result = mt5.order_send(request)
print(order_result)
class="kw">return order_result
class="kw">return &class="macro">#x27;Ticket does not exist&class="macro">#x27;
class="macro">#Update our date from and date to
date_from = class="type">class="kw">datetime(class="num">2023,class="num">11,class="num">1)
date_to = class="type">class="kw">datetime.now()
class="macro">#Get signals from our glass-box model
def ai_signal():
    class="macro">#Fetch OHLC data
    df = pd.DataFrame(mt5.copy_rates_range(market_symbol,TIMEFRAME,date_from,date_to))
    class="macro">#Process the data
    df[&class="macro">#x27;time&class="macro">#x27;] = pd.to_datetime(df[&class="macro">#x27;time&class="macro">#x27;],unit=&class="macro">#x27;s&class="macro">#x27;)
    df[&class="macro">#x27;target&class="macro">#x27;] = (df[&class="macro">#x27;close&class="macro">#x27;].shift(-class="num">1) > df[&class="macro">#x27;close&class="macro">#x27;]).astype(class="type">int)
    preprocess(df)
    class="macro">#Select the last row
    last_close = df.iloc[-class="num">1:,class="num">1:]
    class="macro">#Remove the target column
    last_close.pop(&class="macro">#x27;target&class="macro">#x27;)
    class="macro">#Use the last row to generate a forecast from our glass-box model
    class="macro">#Remember class="num">1 means buy and class="num">0 means sell
    forecast = glass_box.predict(last_close)
    class="kw">return forecast[class="num">0]
class="macro">#Now we define the main body of our Python Glass-box Trading Bot
if __name__ == &class="macro">#x27;__main__&class="macro">#x27;:
    class="kw">while True:
        signal = ai_signal()
        if signal == class="num">1:
            direction = &class="macro">#x27;buy&class="macro">#x27;
        elif signal == class="num">0:
            direction = &class="macro">#x27;sell&class="macro">#x27;
        print(f&class="macro">#x27;AI Forecast: {direction}&class="macro">#x27;)
        if direction == &class="macro">#x27;buy&class="macro">#x27;:
            for pos in mt5.positions_get():
                if pos.type == class="num">1:
                    close_order(pos.ticket)
            if not mt5.positions_totoal():
                market_order(MARKET_SYMBOL,VOLUME,direction)
        elif direction == &class="macro">#x27;sell&class="macro">#x27;:
            for pos in mt5.positions_get():
                if pos.type == class="num">0:

持仓清空后如何补单

这段逻辑处理的是「当前持多单需要先平掉、再等待空仓后重新开仓」的场景。先通过 close_order(pos.ticket) 把指定ticket的买单平仓,紧接着用 mt5.positions_get() 检查账户是否还剩任何持仓。 若 positions_get() 返回空列表,说明场上已无未平仓位,此时调用 market_order(MARKET_SYMBOL, VOLUME, direction) 按预设品种、手数和方向补一张市价单。 循环尾部固定每秒打印一次本地时间并 sleep(60),也就是每 60 秒跑一轮扫描。外汇与贵金属杠杆高,实盘跑这套前建议在 MT5 策略测试器用历史数据验证平仓与补单的触发间隙,避免滑点把逻辑节奏打乱。

MQL5 / C++
class="macro">#This is an open buy order, and we need to close it
close_order(pos.ticket)

if not mt5.positions_get():
    class="macro">#We have no open positions
    market_order(MARKET_SYMBOL,VOLUME,direction)

print(&class="macro">#x27;time: &class="macro">#x27;, class="type">class="kw">datetime.now())
print(&class="macro">#x27;-------\n&class="macro">#x27;)
time.sleep(class="num">60)

◍ 把玻璃盒模型塞进 EA 里跑前瞻测试

把可解释助推机器(EBM)导成 ONNX 后,真正落地是在 MetaTrader 5 里写一个 EA 接管下单与风控。ONNX 运行时能在从数据中心到手机的各种设备上跑模型,MT5 自带的前瞻测试(walk-forward)则是验证模型稳健性的硬手段——它让 EA 跑在模型训练截止日期之后的真实报价上,专门防“用训练集回测骗自己”的自欺行为。 EA 的输入特征只留了 4 个:滞后高度(前一根的 ((H+L)/2−C))、高度增长(高度二阶差分)、中点((H+L)/2)、中点增长(中点二阶差分)。品种也从前半部分的 Boom 1000 Index 换成了 Volatility 75 Index,属于高波动差价合约,爆仓风险显著高于常规外汇对。 OnInit 里加载 ONNX 资源只需三步:建缓冲区、设输入形状、设输出形状。常见坑是只设了一个输入形状却编译通过,运行时才报 5808“张量维度未设置或无效”——模型有 4 个特征就必须逐个索引定义形状。OnTick 分两类逻辑:每次即刻报价更新持仓的 ATR 动态止损止盈;无持仓时仅在新蜡烛形成后调模型预测。 类型匹配是另一道坎。某次用 4 字节 int 数组接 int64 输出,模型需要 8 字节,直接崩出 5807“参数大小无效”。ONNX Run 还要求每个输入各占一个数组,塞进单个数组编译不报错,执行时抛 5804“传递给 OnnxRun 的参数数量无效”。这些错都不会在编译期暴露,只能靠“智能系统”和“日志”选项卡现场抓。 全部接好后用模拟账户跑前瞻测试,图里 EA 在 Volatility 75 上无报错运行。参数随时可调,但任何调参都只是改变 ATR 乘数和加仓阈值,不预示收益。

MQL5 / C++
class=class="str">"cmt">//Meta Properties 
class="macro">#class="kw">property copyright "Gamuchirai Ndawana"
class="macro">#class="kw">property link "https:class=class="str">"cmt">//twitter.com/Westwood267"
class=class="str">"cmt">//Classes for managing Trades And Orders
class="macro">#include  <Trade\Trade.mqh>
class="macro">#include <Trade\OrderInfo.mqh>
class=class="str">"cmt">//Instatiating the trade class and order manager
CTrade trade;

常见问题

玻璃盒把每笔信号拆成可读规则与权重,你能直接看到是哪几个因子触发,避免盲目跟单,适合自己复核逻辑。
用进程间通信把信号传到终端,持仓清空后按最新信号条件判断是否补单,重点校验信号延迟与成交滑点。
小布可自动比对玻璃盒信号与盘面价格行为,标出冲突时段,你只需看摘要决定要不要接进实盘通道。
盯信号触发到成交的时滞、因子权重漂移和样本外命中率,三者异常就先停跑别硬扛。
会,贵金属杠杆高波动大,参数改动后必须重跑样本外检验,否则信号失真概率陡增,属高风险操作。