预测模型训练与部署基础:打通 MT5 与 Python 的推理闭环(基础篇)
🧠

预测模型训练与部署基础:打通 MT5 与 Python 的推理闭环(基础篇)

(1/3)· 从数据落地到 REST 推理,搭建可训练价格行为模型的底层骨架

偏理论进阶 第 1/3 篇
很多交易者把历史数据采完就停在硬盘里,以为有了样本就能自动赚钱。真正缺的是把行情变成特征、把特征变成可调用模型的工程链路。本篇先把这套链路的地基打牢,后面两篇才接得上。

在 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 附近,说明过拟合风险显著,需靠滚动窗口重训。

MQL5 / C++
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 流水线(标准化+梯度提升),否则跳过。

MQL5 / C++
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 只删带这个前缀的对象,换图表不会误清别人的画线。外汇与贵金属波动剧烈、接口延迟可能引发滑点,接真仓前务必在策略测试器跑通再上。

MQL5 / C++
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 跑一周模拟,确认重试节奏再上真实账户。

MQL5 / C++
  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)
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到特征分布与模型调用延迟,你只需专注策略层面的决策。

常见问题

MT5 的 MQL5 环境缺少成熟的机器学习生态,而 Python 拥有 Scikit-learn、TensorFlow 等库,更适合做特征工程与拟合,EA 只负责执行与交互。
Parquet 是列式存储,压缩率高且读取特定字段更快,适合后续特征工程模块批量加载 OHLC 与统计量。
目前小布盯盘内置的是通用特征与信号诊断,不支持外接私有 REST 模型;若需私有推理,可在 MT5 EA 内自行 WebRequest 调用。
概率上不建议,不同品种波动结构差异大,倾向为每个品种单独拟合并缓存专属模型以降低泛化误差。
若在服务端建内存缓存且模型轻量,延迟可能在毫秒级;若网络或推理过重,EA 应设超时与本地兜底逻辑。