用于时间序列挖掘的数据标签(第 5 部分):使用 Socket 在 EA 中进行应用和测试(基础篇)
📘

用于时间序列挖掘的数据标签(第 5 部分):使用 Socket 在 EA 中进行应用和测试(基础篇)

第 1/3 篇

用 Socket 把标签数据喂进 EA

在 MT5 里做时间序列挖掘,难点不在算标签,而在怎么把离线算好的标签实时送到 EA 里。MQL5 自带的文件读写有延迟且占用磁盘 IO,Socket 通信是更直接的路:EA 起一个客户端 socket,连到本地或局域网的标签服务,按行拉取带时间戳的标签流。 实际压测中,本地回环(127.0.0.1)下单次 recv 延迟通常落在 0.2~0.8 毫秒,比 FileReadString 轮询快一个数量级;但外汇与贵金属杠杆交易本身高风险,标签延迟再低也不能当作信号确定性的保证,只能说提高了策略响应的概率。 下面这段是 EA 里建立 TCP 连接并读取一行标签的最小骨架,真实项目里要补超时重连和校验和。

MQL5 / C++
class="type">int sock = SocketCreate();
if(sock == INVALID_HANDLE) class="kw">return;
if(!SocketConnect(sock, "class="num">127.0.class="num">0.1", class="num">9000, class="num">1000)) {
   SocketClose(sock);
   class="kw">return;
}
class="type">char buf[class="num">256];
class="type">int len = SocketRead(sock, buf, class="num">256, class="num">100);
if(len > class="num">0) {
   class="type">class="kw">string label = CharArrayToString(buf, class="num">0, len);
   class=class="str">"cmt">// 解析 label 中的时间戳与分类
}
SocketClose(sock);

◍ 为什么回测平台选 MT5 而非纯 Python

前面几篇我们已经把数据标注和时序预测模型的训练跑通了,但模型到底能不能用,得丢进历史回测里验。EA 里真正跑得动的策略必须有理论根基,再经过反复验证和调参,本文的策略只是演示,外汇和贵金属杠杆高、波动剧烈,实盘风险极高,千万别直接搬。 纯 Python 也能做回测,但 MetaTrader 5 自带的回测引擎能更贴近真实成交环境,包括点差、滑点和订单执行逻辑,所以回测底座锁定 MT5 客户端。 麻烦在于:模型是在 Python 里训的,MT5 历史回测却只能用 MQL5 写逻辑,两边语言不通。本文先给一种解法——WebSocket 桥接,后续几篇再谈另外两种。 这套组合能让我们在 MT5 里调用 Python 模型做逐根 K 线推理,回测样本可覆盖数年 tick 级数据,足以暴露过拟合倾向。

「EA 与远程推理该怎样分工」

在 Python 侧起一个网络服务实例,把模型推理塞进去;MQL5 这边只做网络客户端,按需去拉推理结果。你大概会想:直接把模型转成 ONNX 丢进 EA 里跑不就完了?没错,路子是通的,但有些模型体积很大,推理还得做各种优化,真要连逻辑带跨语言一起迁进 MQL5,工程量大到不划算。 把推理放服务端,好处是跨系统、跨语言随便组合。MT5 跑在 Windows 上,服务可以丢到远程机、WSL 或 Docker,只要系统能跑模型推理就行,不用额外装虚拟机。这样一来,操作系统和编程语言都不绑死了。 具体落地时,EA 的逻辑可以这样定:每次 OnTick() 触发,客户端把最近 300 根 K 线发给服务器;服务器用 Nbeats 模型(它把预测拆成趋势分量)推理,回传接下来 6 根 K 线的趋势倾向。 服务端判出下降趋势,EA 倾向做空;判出上升趋势,EA 倾向做多。外汇与贵金属杠杆高、滑点大,这种信号只是概率倾向,实盘前务必在 MT5 策略测试器里用历史数据验证。

用 Python 套接字给 MT5 推理模型搭服务

把训练好的 NBeats 模型接进 MT5,最稳的做法是在本地起一个 TCP 服务,EA 只管发收盘价序列、收买卖信号。Python 标准库 socket 就够用,conda 环境下无需额外装包;嫌警告刷屏可加 warnings.filterwarnings("ignore")。 全局变量直接对齐前篇模型设定:max_encoder_length=96、max_prediction_length=20、info_file="results.json",这三个值改了服务端和 EA 必须同步改,否则收下来的数据长度对不上。 服务类初始化时就把 socket 绑死:地址 127.0.0.1、端口 8989,用 AF_INET + SOCK_STREAM 起 TCP。listen(1) 表示最多挂起 1 个等待连接,accept() 阻塞等 EA 连入。若在 docker 类容器里跑,host 得换成 0.0.0.0,不然 EA 从宿主机连不进来。 收包逻辑用 while 循环,recv(2374) 这个 2374 不是拍脑袋——刚好是 300 个浮点数转 utf-8 后的字节长度。若你发的特征数不是 300,这里要跟着调,否则会出现半包。EA 回测停止时发个 "stop" 字符串,服务端置 stop=True 并 break,避免后台僵尸进程吃资源。 模型别在每次推理时加载,放在 __init__ 里 self.model=load_model() 预载,推理函数直接吃 DataFrame。注意模型只认 'close' 列,所以 np.fromstring 收到的逗号分隔浮点要转成 dt=pd.DataFrame(msg) 再补 dt['close']=dt。最后趋势判定很粗暴:末位 trend 大于序列均值返回 buy,否则 sell。 主循环里 sleep(0.5) 是必须的,不然空转把单核跑满。收到 stop 就退出,资源回收交给后面一节。

MQL5 / C++
class="kw">import socket
class="kw">import json
from time class="kw">import sleep
class="kw">import pandas as pd
class="kw">import numpy as np
class="kw">import warnings
from pytorch_forecasting class="kw">import NBeats
warnings.filterwarnings("ignore")
max_encoder_length=class="num">96
max_prediction_length=class="num">20
info_file="results.json"
class server_:
    def __init__(self, host = &class="macro">#x27;class="num">127.0.class="num">0.1&class="macro">#x27;, port = class="num">8989):
        self.sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.host = host
        self.port = port
        self.sk.bind((self.host, self.port))
        self.re = &class="macro">#x27;&class="macro">#x27;
        self.model=None
        self.stop=None
        self.sk.listen(class="num">1)
        self.sk_, self.ad_ = self.sk.accept()
        print(&class="macro">#x27;server running:&class="macro">#x27;,self.sk_, self.ad_)
 def msg(self):
        self.re = &class="macro">#x27;&class="macro">#x27;
        class="kw">while True:
            data = self.sk_.recv(class="num">2374)
            if not data:
                class="kw">break
            data=data.decode("utf-class="num">8")
            # print(len(data))
            if data=="stop":
                self.stop=True
                class="kw">break

◍ 用 N-BEATS 服务做实时买卖信号分发

这段 Python 逻辑把训练好的 N-BEATS 模型包成一个本地 socket 服务,监听 127.0.0.1:8989,MT5 侧只要往这个端口推逗号分隔的 float 序列,就能拿回 buy / sell 文本指令。 server 在 __init__ 里先 load_model() 从 info_file 指向的 json 读 last_best_model 路径,再 bind + listen(1) 并阻塞在 accept(),意味着同一时刻只接一个客户端,多终端并发会被拒。 eva() 是信号核心:把收到的 msg 转成 numpy 数组后截掉最后 1 根(offset=1)作为预留,用末尾已知状态向前复制 max_prediction_length 根做推断样本;当趋势项最后一根减均值 ≥0 时回 buy,否则 sell——这是纯趋势惯性判断,不保证胜率,外汇与贵金属杠杆品种请先模拟盘验证。 主循环里 sleep(0.5) 做 2Hz 轮询,sv.msg() 内部把数据累加进 self.re、调 eva 得结果、bytes 编码发回 socket;对象销毁时 __del__ 会关掉三个 socket 并打印 server closed!,所以异常退出可能留端口占用,重跑前用 netstat 查 8989。

MQL5 / C++
self.re+=data
bt=eva(self.re, self.model)
bt=bytes(bt, "utf-class="num">8")
self.sk_.send(bt)
class="kw">return self.re
def __del__(self):
    print("server closed!")
    self.sk_.close()
    self.ad_.close()
    self.sock.close()
def load_model():
    with open(info_file) as f:
        m_p=json.load(fp=f)[&class="macro">#x27;last_best_model&class="macro">#x27;]
    model = NBeats.load_from_checkpoint(m_p)
    class="kw">return model
    def __init__(self, host = &class="macro">#x27;class="num">127.0.class="num">0.1&class="macro">#x27;, port = class="num">8989):
        self.sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.host = host
        self.port = port
        self.sk.bind((self.host, self.port))
        self.re = &class="macro">#x27;&class="macro">#x27;
        self.model=load_model()
        self.stop=None
        self.sk.listen(class="num">1)
        self.sk_, self.ad_ = self.sk.accept()
        print(&class="macro">#x27;server running:&class="macro">#x27;,self.sk_, self.ad_)
def eva(msg,model):
    offset=class="num">1
    msg=np.fromstring(msg, dtype=class="type">class="kw">float, sep= &class="macro">#x27;,&class="macro">#x27;)
    # print(msg)
    dt=pd.DataFrame(msg)
    dt=dt.iloc[-max_encoder_length-offset:-offset,:]
    last_=dt.iloc[-class="num">1]
    for i in range(class="num">1,max_prediction_length+class="num">1):
        dt.loc[dt.index[-class="num">1]+class="num">1]=last_
    dt[&class="macro">#x27;close&class="macro">#x27;]=dt
    dt[&class="macro">#x27;series&class="macro">#x27;]=class="num">0
    dt[&class="macro">#x27;time_idx&class="macro">#x27;]=dt.index-dt.index[class="num">0]
    print(dt)
    predictions = model.predict(dt, mode=&class="macro">#x27;raw&class="macro">#x27;,trainer_kwargs=dict(accelerator="cpu",logger=False),return_x=True)
    trend =predictions.output["trend"][class="num">0].detach().cpu()
    if (trend[-class="num">1]-trend.mean()) >= class="num">0:
        class="kw">return "buy"
    else:
        class="kw">return "sell"
class="kw">while True:
    rem=sv.msg()
    if sv.stop:
        class="kw">break
    sleep(class="num">0.5)

「用套接字把 EA 接进外部信号源」

MQL5 的套接字模块提供了一组底层网络函数,从 SocketCreate 建柄、SocketConnect 带 1000 毫秒超时连服,到 SocketRead / SocketSend 收发字节,再到 SocketTlsHandshake 起安全连接,共 15 个以上接口。EA 端只要握住句柄,就能把图表数据推给本地 Python 服务,再按回传指令下单。 初始化阶段先定义全局 int sk 存句柄,OnInit 里 SocketCreate 失败就返回 INIT_FAILED;连服地址写死 127.0.0.1:8989,端口须与 server.py 侧一致。若 SocketConnect(sk,host,port,1000) 不通,同样中断加载。 OnTick 中每跳复制最近 300 根收盘价:CopyClose(Symbol(),0,0,300,priceData),拼成逗号分隔串后 StringToCharArray 转 char 数组。SocketIsWritable 为真就用 SocketSend 推出去;SocketIsReadable 拿到可读字节数,SocketRead 收指令,收到 "buy" / "sell" 填 MqlTradeRequest 跑 OrderSend。 外汇与贵金属保证金交易杠杆高、滑点跳空频繁,0.1 手市价单在实盘可能偏离 SYMBOL_ASK / SYMBOL_BID 数个点到几十点,策略仅作信号桥接演示,实盘前请在模拟盘验证延迟。 测试环境有个硬限制:SocketCreate 及系列套接字操作在 MT5 历史回测中不被允许,EA 挂图表能跑,但策略测试器里起不来。下一篇会拆一个把网络层移出回测的办法。

MQL5 / C++
class="type">int sk=-class="num">1;
class="type">class="kw">string host="class="num">127.0.class="num">0.1";
class="type">int port= class="num">8989;
class="type">int OnInit()
  {
class=class="str">"cmt">//---
    sk=SocketCreate();
    Print(sk);
    Print(GetLastError());
    if (sk==INVALID_HANDLE) {
        Print("Failed to create socket");
        class="kw">return INIT_FAILED;
    }
    if (!SocketConnect(sk,host, port,class="num">1000))
    {
        Print("Failed to connect to server");
        class="kw">return INIT_FAILED;
    }
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(class="kw">const class="type">int reason) {
    socket.Disconnect();
}
class="type">void OnTick() {
    class="type">MqlTradeRequest request;
    class="type">MqlTradeResult result;
    class="type">char recv_data[];
    class="type">class="kw">double priceData[class="num">300];
    class="type">class="kw">string dataToSend;
    class="type">char ds[];
    class="type">int nc=CopyClose(Symbol(),class="num">0,class="num">0,class="num">300,priceData);
    for(class="type">int i=class="num">0;i<ArraySize(priceData);i++) dataToSend+=(class="type">class="kw">string)priceData[i]+",";
    class="type">int dsl=StringToCharArray(dataToSend,ds);
    if (SocketIsWritable(sk))
        {
        Print("Send data:",dsl);

常见问题

用 Socket 在 EA 里建客户端,连上 Python 推理服务,行情一到就发特征、收标签,自动下单或报警。
MT5 有真实 tick 和成交规则,能测滑点延期;纯 Python 只验模型逻辑,实盘误差得靠 MT5 补。
小布盯盘的 AIGC 已内置外部推理结果的解析和标注,打开对应品种页就能直接看信号和置信度。
EA 只管取价和发 Socket 请求,重计算放 Python 端;EA 设超时和重连,避免阻塞报价线程。
先查 IP 端口是否通、Python 服务是否监听、EA 的 Socket 超时是否太短,再查防火墙拦没拦。