多层感知器和反向传播算法(第 3 部分):与策略测试器集成 - 概述(I)(基础篇)
把 MLP 塞进 MT5 策略测试器
在 MT5 里跑多层感知器(MLP),最直接的办法是借助策略测试器做批量前向推理与参数扫描。原生测试器本身不认神经网络结构,需要把网络权重作为外部参数或文件读入,才能在每根 K 线或每笔 tick 上调用前向传播。 原文给出的工程切入口是:用 EA 框架承载 MLP 推理逻辑,测试器负责调度不同样本窗口与隐藏层规模的组合。这样一次回测就能横向比出哪组神经元数在给定品种上更稳。 需要注意,外汇与贵金属杠杆高、滑点跳空频繁,测试器结果只是样本内概率倾向,实盘前务必用不同年份数据交叉验证。
◍ 为什么不能直接拿 Python 模型跑 MT5 回测
早前按客户端-服务器简化连接去用机器学习模型,只能放在实盘生产环境跑,因为 MT5 测试器根本不支持网络函数执行。想在测试器外验证策略,就得先在 Python 脚本里把模型训好再判买卖点,但这套路有个硬伤:用来当过滤器或算信号的技术指标,若属自定义或闭源,Python 端往往复刻不了。 当策略依赖自定义指标过滤,或要测带止盈止损、尾随停止、盈亏平衡的逻辑时,缺了策略测试器参与就很要命。自己从零搭一个测试器,哪怕用 Python 这种相对友好的语言,也是相当严峻的工程挑战。 对外汇、贵金属这类高波动品种来说,绕开原生测试器意味着样本外风险没法系统量化,模型在未知行情里翻车的概率会明显抬高。
「用消息传递打通 Python 与 MQL5」
要做回归和分类模型,又想直接在 Python 里跑,最省事的办法不是把模型塞进 EA,而是让 MT5 只负责行情和下单,把特征计算和推理甩给外部 Python 进程。 这里采用一套同步消息传递机制:Python 侧作为服务端常驻监听,MQL5 侧作为客户端在每根 K 线或每次信号触发时发起连接、发送特征、回收预测。两侧通过交换结构化消息协同,而不是共享内存或文件轮询。 对交易者而言,这意味着你可以在 MT5 里只保留极薄的取数逻辑,复杂逻辑全放 Python;验证路径也很直接——开 MT5 跑一个最小客户端,用本地 Python socket 回显,先确认两端能握手再接模型。外汇与贵金属杠杆高,任何信号延迟都可能放大滑点,实盘前务必在模拟环境压测消息往返耗时。
用 CSV 打通 MT5 与 Python 的初始化握手
MT5 的沙箱机制把 MQL5 程序锁死在平台内部,WebRequest 在策略测试器里直接失效,文档白纸黑字写着不能跑。想做 EA 和外部 Python 推理服务的集成,命名管道是最干净的路子,但纯实验阶段没必要上重武器,先拿 CSV 文件做进程间消息交换反而最省事。 MQL5 标准文件类 CFileTxt 能写各种类型和数组,只是不自带 CSV 表头写入,这个缺口自己补一行就完事。下面的宏负责生成初始化消息标头,把 host、port、typerun 三个字段塞进 init.csv,EA 加载到图表后才触发第一次握手。 环境判定靠 MQLInfoInteger(MQL_TESTER) 和 MQL_VISUAL_MODE,命中就标 TEST,否则 LIVE;InpHost 默认 127.0.0.1、InpPort 默认 8081,都是本地回环,外汇与贵金属自动化涉及杠杆交易,实盘连接前务必先在测试环境跑通。Python 端读到 init.csv 后回写 init_checked.csv,EA 侧用 while 轮询文件存在性来等确认,这段阻塞逻辑在测试器里也能正常跑。 代码里 File.SetCommon(true) 把文件丢进终端公共目录,跨进程才看得到;WriteHeader 和 WriteLine 连续调用,任一步返回小于 1 就判失败。你开 MT5 把这段代码贴进 EA,把 PATH_COMMON 指到 Terminal/Common/Files/TransferML,就能在文件管理器里看到握手前后的两个 CSV。
class="macro">#define HEADER_FILE_INIT {"host","port","typerun"} class="macro">#define LINES_FILE_INT(HOST, PORT, TYPE) {{class="type">class="kw">string(HOST), class="type">class="kw">string(PORT), class="type">class="kw">string(TYPE)}} sinput group "General Configuration" sinput class="type">class="kw">string InpHost = "class="num">127.0.class="num">0.1"; sinput class="type">int InpPort = class="num">8081; class="kw">static EtypeRun typerun= (MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))?TEST:LIVE; if(!monitor.OnInit(typerun, InpHost, InpPort)) class="kw">return(INIT_FAILED); class="type">bool CMonitor::OnInit(EtypeRun type_run, class="type">class="kw">string host, class="type">int port) { ... File.SetCommon(true); File.Open("TransferML/init.csv", FILE_WRITE|FILE_SHARE_READ|FILE_ANSI); class="type">class="kw">string header[class="num">3] = HEADER_FILE_INIT; class="type">class="kw">string lines[class="num">1][class="num">3] = LINES_FILE_INT(host,port,type_run); if((File.WriteHeader(header)<class="num">1&File.WriteLine(lines)<class="num">1&!Strategy.Config(m_params))!=class="num">0) res=class="kw">false; File.Close(); ... } host, port, typerun = file.check_init_param(PATH_COMMON.format(INIT_ARCHIVE)) file.save_file_csv(PATH_COMMON.format(INIT_OK_ARCHIVE)) class="type">bool CMonitor::OnInit(EtypeRun type_run, class="type">class="kw">string host, class="type">int port) { ... class="kw">while(!File.IsExist("TransferML/init_checked.csv", FILE_COMMON)) { class=class="str">"cmt">//waiting for startup Comment("waiting for startup"); } ... }