利用 Python 和 MQL5 构建您的第一个玻璃盒模型·进阶篇
「为什么交易系统该用玻璃盒」
玻璃盒模型把决策逻辑完全摊开,不像黑盒那样只能看输入输出。对做价格行为分析的人而言,能直接读懂模型在哪根 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 偏蓝(反向拖累),而价差-高度交汇项为橙(指向正确)。这类局部贡献在外汇高风险环境下值得单根复核,别直接当信号。
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,说明通道通了。 外汇与合成指数品种杠杆高、滑点跳空频繁,最小交易量随经纪商变,真跑之前先在策略测试器用演示账户验一遍返回结构再接实盘。
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 线。
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 策略测试器用历史数据验证平仓与补单的触发间隙,避免滑点把逻辑节奏打乱。
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 乘数和加仓阈值,不预示收益。
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;