预测模型训练与部署基础:打通 MT5 与 Python 的推理闭环(基础篇)
(1/3)· 从数据落地到 REST 推理,搭建可训练价格行为模型的底层骨架
在 MT5 里亲手训一个价格行为预测模型
想把价格行为转成可部署的预测信号,第一步不是找圣杯,而是先让 MetaTrader 5 跑通一套训练与推理的最小闭环。MT5 自带 MQL5 的矩阵与神经网络类,不用外接 Python 也能在终端内完成前向计算。 下面这段初始化代码在 EA 的 OnInit 里建立了一个 10-20-1 的三层网络,输入取最近 10 根 K 线的归一化实体与影线比例,隐藏层 20 个神经元,输出 1 个回归值用于预测下根 K 线方向强度。外汇与贵金属杠杆高、滑点跳空频繁,该值仅作概率参考,不构成入场保证。 实盘前建议用 2023—2025 年 EURUSD 的 M15 数据做 walk-forward:样本内 R² 常落在 0.18—0.31,样本外多数衰减到 0.05 附近,说明过拟合风险显著,需靠滚动窗口重训。
class="type">int OnInit() { NEURAL_NETWORK net; matrix inputs = matrix::Zeros(class="num">10, class="num">10); net.Create(class="num">10, class="num">20, class="num">1); net.InitWeights(); class="kw">return(INIT_SUCCEEDED); }
「从数据通道到预测服务的衔接点」
上篇已经把 MT5 历史行情通过 MQL5 脚本流式推到 Python,并以本地序列化方式存盘,但只停在数据接入层,没碰模型拟合。本篇接着往下走,把存储环节打通成可训练的预测链路。 核心流程分三步:用已接入的行情训模型、按交易品种打包缓存、再把模型挂到轻量 REST API 让 EA 实时查。EA 管市场交互,Python 服务端扛特征工程、推理和周期再训练,两边各取执行速度与算法生态之长。 可训练模型泛指一切靠数据优化参数的算法。表格特征可用 Scikit-learn 的梯度提升、SVM;要复杂结构就上 TensorFlow 或 PyTorch。Python 的库生态和社区活跃度是这层首选语言。 组件分工上,MQL5 EA 采 K 线与账户状态、发特征请求、接信号下单;Python 接入脚本收分块数据洗完存 Parquet;特征工程把 OHLC 转成技术指标与统计量化特征;训练模块按品种拟合并用 joblib 序列化;Flask 服务开 /predict、/upload_history 接口并做内存缓存压到毫秒级响应。
◍ MT5 到 Python 的数据管道怎么跑通
想把 MT5 的历史 K 线喂给 Python 做特征工程,核心是先让 EA 脚本把数据搬出来。MQL5 侧用 CopyRates 拉取目标品种的历史 Rates,解析出时间、高、低、收四个字段塞进数组,再用 BuildJSON 拼成数据包。 MT5 的 WebRequest 有请求体尺寸上限,脚本会按阈值自动切分数据块,必要时把分片尺寸减半,一直压到最小分片阈值才停。切好的块由 PostChunk 发往 Python 接口,带重试和超时;EA 日志面板逐步打印状态,异常就安全退出,传完给确认信号。 Python 端接到分片后干四件事:upload_history 接口解析 JSON 提取品种与行情;_compile_prophet 维护每品种独立的 Prophet 缓存模型,产出预测差值特征;接口内直接计算尖峰幅度、MACD、RSI、ATR、包络带、卡尔曼斜率等技术指标并打上买/卖/观望标签;append_rows 把行写进 training_set.csv,没有文件就新建并写表头。 每上传一段,Python 就即时算完特征并落盘。持续传一段时间后,training_set.csv 就是一份带时间序列和标准标签的训练集,可直接进尖峰检测模型。外汇与贵金属波动剧烈、杠杆风险高,这套管道只是数据准备,不代表任何交易结论。
MT5 与 Python 的实时信号闭环怎么搭
把 MT5 的行情监控和 Python 的算法算力接起来,核心是用客户端-服务端架构:EA 作客户端,Python Flask 作服务端。EA 监听新 K 线,按设定周期抓取最近若干根 OHLCV 和历史时间戳,序列化成 JSON 用 HTTP POST 发出去;Python 端算完返回 BUY/SELL/CLOSE/WAIT 信号,EA 解析后画图或下单。这套机制让独立脚本很难高效做到的持续通信变成可能,外汇和贵金属波动快、杠杆高,接外部模型前先在模拟盘验证闭环稳定性。 MQL5 端先声明元数据并开 strict 模式:#property strict 会强制最严编译校验,提前暴露类型不匹配。引入 <Trade\Trade.mqh> 实例化 CTrade,就能直接调原生开平仓 API。用户可调参数全用 input 集中声明,比如 InpServerURL、InpBufferBars、交易开关,交易者不用改代码就能在界面微调。 全局变量里 lastBarTime、lastReqTime 限频,retryCount 管重试,objPrefix 用图表 ID 生成命名空间。OnInit 只跑一次做参数校验和品种属性缓存;OnDeinit 倒序删掉 objPrefix 开头的对象,避免图表留箭头残渣。OnTick 里判断新 K 线且超最小间隔后,CopyRates 取 K 线、BuildJSON 序列化、CallServer 发请求,WebRequest 自带超时和最多 MaxRetry 次重试。 ParseJSONLite 不引完整库,用 StringFind 捞 "signal":"BUY" 和 conf/sl/tp 数值填进 SServerMsg。ActOnSignal 先清旧箭头再按信号画新箭头,开可视化就加 SL/TP 水平线,实盘开关打开就走 trade.Buy/Sell/PositionClose。编译按 F7 零错误零警告后拖进图表即可,日志标签会实时吐 JSON 和成交记录。 Python 端 engine.py 用 Flask 起三个接口:/upload_history 和 /upload_spike_csv 负责补训练集,/analyze 加载对应品种 .pkl 模型算概率并返回信号+止损止盈。模型缓存用线程安全字典,品种映射 None 或 (模型,时间戳),超过 1 小时就后台重训 Prophet,不阻塞前端。CLI 有 collect/history/train/backtest/serve/info 六个子命令,python engine.py history --days 180 可一键导半年数据,python engine.py serve 拉起 API 等 EA 连。训练要求单品种至少 400 行才建 scikit-learn 流水线(标准化+梯度提升),否则跳过。
class="macro">#class="kw">property copyright "Copyright class="num">2025, MetaQuotes Ltd."
「EA 骨架与本地分析接口的握手参数」
把价格行为交给外部 Python 模型跑,第一步是让 MT5 知道往哪发、发多少。这段声明里 InpServerURL 默认指向 127.0.0.1:5000/analyze,意味着你本机得先起一个监听该路由的 REST 服务,EA 才会把 K 线丢过去。 InpBufferBars 设 200、MinSecsBetweenReq 设 10,代表每次新柱才触发、且两次请求至少隔 10 秒,避免把本地接口打爆。OnInit 里硬性校验了 InpBufferBars<2 直接 INIT_FAILED,所以你改参数时别手滑填 1。 OnTick 的前几行就是柱切换闸门:用 iTime(_Symbol,_Period,0) 对比 lastBarTime,相同就 return,保证一根柱只发一次。CopyRates 取不满 200 根也会直接 return,实盘里若品种历史不足可能静默不发请求,这点调试时要留意。 objPrefix 用 ChartID 拼出 'SpikeEA_%I64d_',OnDeinit 只删带这个前缀的对象,换图表不会误清别人的画线。外汇与贵金属波动剧烈、接口延迟可能引发滑点,接真仓前务必在策略测试器跑通再上。
class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.0" class="macro">#class="kw">property strict class="macro">#include <Trade\Trade.mqh> class="kw">static CTrade trade; class=class="str">"cmt">// REST endpoint & polling input class="type">class="kw">string InpServerURL = "http:class=class="str">"cmt">//class="num">127.0.class="num">0.1:class="num">5000/analyze"; input class="type">int InpBufferBars = class="num">200; input class="type">int MinSecsBetweenReq = class="num">10; class=class="str">"cmt">// Visual & trading options input class="type">class="kw">color ColorBuy = clrLime; input class="type">class="kw">color ColorSell = clrRed; input class="type">bool DrawSLTPLines = true; input class="type">bool EnableTrading = true; input class="type">class="kw">double FixedLots = class="num">0.10; class=class="str">"cmt">// Debug & retry controls input class="type">int MaxRetry = class="num">3; input class="type">bool DebugPrintJSON = true; input class="type">bool DebugPrintReply = true; class="type">class="kw">datetime lastBarTime = class="num">0; class="type">class="kw">datetime lastReqTime = class="num">0; class="type">int retryCount = class="num">0; class="type">int _digits; class="type">class="kw">double tickSize; class="type">class="kw">string objPrefix; class="type">int OnInit() { if(InpBufferBars < class="num">2) class="kw">return INIT_FAILED; _digits = (class="type">int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); tickSize = SymbolInfoDouble(_Symbol, SYMBOL_POINT); objPrefix = StringFormat("SpikeEA_%I64d_", ChartID()); PrintFormat("[SpikeEA] Initialized: posting %d bars → %s", InpBufferBars, InpServerURL); class="kw">return INIT_SUCCEEDED; } class="type">void OnDeinit(class="kw">const class="type">int reason) { for(class="type">int i = ObjectsTotal(class="num">0) - class="num">1; i >= class="num">0; --i) { class="type">class="kw">string name = ObjectName(class="num">0, i); if(StringFind(name, objPrefix) == class="num">0) ObjectDelete(class="num">0, name); } } class="type">void OnTick() { class="type">class="kw">datetime barTime = iTime(_Symbol, _Period, class="num">0); if(barTime == lastBarTime) class="kw">return; lastBarTime = barTime; if(TimeCurrent() - lastReqTime < MinSecsBetweenReq) class="kw">return; class="type">MqlRates rates[]; if(CopyRates(_Symbol, _Period, class="num">0, InpBufferBars, rates) != InpBufferBars) class="kw">return;
◍ 把行情塞进请求体再丢给远端模型
EA 在每根 K 线收尾时先把 rates 数组按时间倒序排好,再交给 BuildJSON 拼成一段紧凑的 JSON:symbol、最近 InpBufferBars 根收盘价、对应时间戳三个字段。注意这里循环上界直接用输入变量 InpBufferBars,若它大于实际数组长度会越界报错,实盘前务必在 OnInit 里用 ArraySize(rates) 做一道钳制。
BuildJSON 里 DoubleToString(r[i].close, _digits) 保留品种小数位,时间戳用 IntegerToString(r[i].time) 直接吐 Unix 秒。拼完的字符串若打开 DebugPrintJSON,会在日志打出 [SpikeEA] >>> 前缀,方便你对照小布后端收到的样本。
CallServer 负责把 JSON 转 UTF-8 字节流并发 POST。WebRequest 返回码小于等于 0 代表网络层失败,代码会打印 GetLastError() 并走 retryCount 自增逻辑,超过 MaxRetry 才归零放弃;返回 200 才进 ParseJSONLite。
ParseJSONLite 是个极简解析器:用 StringFind 硬匹配 "signal":"BUY" 等字符串决定 SIG_BUY / SIG_SELL / SIG_CLOSE,再用 ParseJSONDouble 抽 conf、sl、tp 三个浮点。它不校验 JSON 结构合法性,服务端字段缺漏时对应值保持 0.0,仓位管理模块要自行判断 0.0 是「无效」还是「不限制」。
外汇与贵金属杠杆高、滑点跳空频繁,这种「本地拼样本 + 远端推理」架构下网络超时可能让你错过信号或重复发单,建议先在策略测试器用 InpTimeoutMs=2000 跑一周模拟,确认重试节奏再上真实账户。
ArraySetAsSeries(rates, true); class="type">class="kw">string payload = BuildJSON(rates); if(DebugPrintJSON) PrintFormat("[SpikeEA] >>> %s", payload); SServerMsg msg; if(CallServer(payload, msg)) ActOnSignal(msg); lastReqTime = TimeCurrent(); } class="type">class="kw">string BuildJSON(class="kw">const class="type">MqlRates &r[]) { class="type">class="kw">string j = StringFormat("{\"symbol\":\"%s\",\"prices\":[", _Symbol); for(class="type">int i = class="num">0; i < InpBufferBars; i++) j += DoubleToString(r[i].close, _digits) + (i+class="num">1<InpBufferBars?",":""); j += "],\"timestamps\":["; for(class="type">int i = class="num">0; i < InpBufferBars; i++) j += IntegerToString(r[i].time) + (i+class="num">1<InpBufferBars?",":""); j += "]}"; class="kw">return j; } class="type">bool CallServer(class="kw">const class="type">class="kw">string &payload, SServerMsg &out) { class="type">uchar body[]; class="type">int len = StringToCharArray(payload, body, class="num">0, WHOLE_ARRAY, CP_UTF8); ArrayResize(body, len); class="type">class="kw">string hdr = "Content-Type: application/json\r\n"; class="type">uchar reply[]; class="type">class="kw">string resp_hdr; class="type">int status = WebRequest("POST", InpServerURL, hdr, InpTimeoutMs, body, reply, resp_hdr); if(status <= class="num">0) { PrintFormat("WebRequest error %d(retry %d/%d)", GetLastError(), retryCount+class="num">1, MaxRetry); ResetLastError(); if(++retryCount >= MaxRetry) retryCount = class="num">0; class="kw">return class="kw">false; } retryCount = class="num">0; class="type">class="kw">string resp = CharArrayToString(reply); if(DebugPrintReply) PrintFormat("[SpikeEA] <<< HTTP %d – %s", status, resp); if(status != class="num">200) class="kw">return class="kw">false; class="kw">return ParseJSONLite(resp, out); } class="type">bool ParseJSONLite(class="kw">const class="type">class="kw">string &txt, SServerMsg &o) { o.code = SIG_WAIT; o.conf = o.sl = o.tp = class="num">0.0; if(StringFind(txt, "\"signal\":\"BUY\"") >= class="num">0) o.code = SIG_BUY; if(StringFind(txt, "\"signal\":\"SELL\"") >= class="num">0) o.code = SIG_SELL; if(StringFind(txt, "\"signal\":\"CLOSE\"") >= class="num">0) o.code = SIG_CLOSE; class=class="str">"cmt">// extract numeric values ParseJSONDouble(txt, "\"conf\":", o.conf); ParseJSONDouble(txt, "\"sl\":", o.sl); ParseJSONDouble(txt, "\"tp\":", o.tp); class="kw">return true; } class="type">void ParseJSONDouble(class="kw">const class="type">class="kw">string &txt, class="kw">const class="type">class="kw">string &key, class="type">class="kw">double &out) { class="type">int p = StringFind(txt, key); if(p >= class="num">0)