市场模拟(第 11 部分):套接字(五)·进阶篇
📘

市场模拟(第 11 部分):套接字(五)·进阶篇

第 2/2 篇

◍ 用 Python 搭一个单机回声服务

先说清这个服务的定位:它只是验证 Excel 通过 xlwings 调起 Python 套接字的最小可行样本,别指望拿去实盘接行情。代码跑在 127.0.0.1:27015,客户端和服务端必须同机,物理或虚拟环境都行,但得在同一条网络命名空间里。 第 07 行 bind 的地址写成 127.0.0.1,意味着只收本地回环连接;若想放开给任意地址,把值改成 "0.0.0.0" 即可,socket 会接受所有客户端网段。但注意 listen(1) 写死在代码里,服务端同一时刻只容一个连接,第二个客户端会被拒,直到当前连接断开服务端才退出。 Excel 侧的状态回写很直白:A1 写等待、A2 写客户端地址、A3 写断开,分别对应代码第 08、12、18 行。VBA 里用 Worksheet_Activate 触发 RunPython,第二次切回工作表会再调一次,此时 Python 抛的并非真错误,只是没做“是否已运行”的判断。 想验证就两步走:MT5 或任意 socket 客户端连 127.0.0.1:27015,发字符串应原样带 "Echo Server:" 前缀返包;同时看 Excel 的 A1~A3 是否按连接生命周期刷新。外汇与贵金属交易接口若沿用此单机模型,需注意高并发场景下单连接限制会直接拖垮采价,属高风险架构点。

MQL5 / C++
class="kw">import socket
class="kw">import xlwings as xw

def Echo():
    wb = xw.Book.caller()
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(("class="num">127.0.class="num">0.1", class="num">27015))
    wb.sheets[class="num">0][&class="macro">#x27;A1&class="macro">#x27;].value = "Waiting connection..."
    server.listen(class="num">1)
    client, addr = server.accept()
    client.send("Wellcome to Server in Python\n\r".encode())
    wb.sheets[class="num">0][&class="macro">#x27;A2&class="macro">#x27;].value = str(f"Client connected by {addr}")
    class="kw">while True:
        info = client.recv(class="num">512)
        if not info:
            class="kw">break
        client.send(b"Echo Server:" + info)
    wb.sheets[class="num">0][&class="macro">#x27;A3&class="macro">#x27;].value = "Client disconnected"
    client.close()

Private Sub Worksheet_Activate()
    MsgBox "Call Server..."
    RunPython "class="kw">import Server_Echo; Server_Echo.Test()"
End Sub

MT5 里跑通回声客户端

把 Python 先放一边,直接在 MQL5 里写客户端更利于后续扩展。MetaTrader 5 里只有指标类应用不能开套接字,服务(service)、EA、脚本都能自由使用 Socket,所以这里用 #property service 声明一个后台服务最合适。 动手前有一处必须配置:MT5 选项里要勾选「允许自动交易」下的套接字权限,并指定接收方地址为 127.0.0.1、端口 27015,否则 SocketConnect 会直接返回失败。代码逻辑和之前几篇的套接字示例高度相似,核心差异在第 50 行附近——当收到空或异常数据时主动断开,对应 Python 服务端第 15 行收到脏数据就关闭连接的写法。 服务启动后,客户端每隔一个本地时间字符串就通过 SocketSend 推给服务端,终端里会实时打印「To Server: 2024.05.20 14:33:01」这类回显,Excel 侧的 Python 服务端同步可见。外汇与贵金属交易本身高风险,这类本地回环测试仅验证通信链路,不涉及任何实盘信号。 别把指标当套接字容器 MT5 指标类型硬限制开不了 Socket,想做终端与外程序通信只能落 service 或 EA,选 service 可常驻不占用图表。

MQL5 / C++
class="macro">#class="kw">property service
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property description "Echo Server Test Service."
class="macro">#class="kw">property description "Requires that the Python server is running in Excel."
class="macro">#class="kw">property version   "class="num">1.00"

class="kw">input class="type">class="kw">string     user00 = "class="num">127.0.class="num">0.1";   class=class="str">"cmt">//Address
class="kw">input class="type">int        user01 = class="num">27015;          class=class="str">"cmt">//Port

class="type">void OnStart()
{
   class="type">char buff[], resp[];
   class="type">int sock = SocketCreate(), ret;
   class="type">uint len;
   class="type">class="kw">string szMsg;
   
   if (sock == INVALID_HANDLE)
   {
      Print("Unable to create socket. Error: ", GetLastError());
      class="kw">return;
   }
   if (!SocketConnect(sock, user00, user01, class="num">1000))
   {
      Print("Connection with the address [", user00, "] in port ", user01, " failed. Error code: ", GetLastError());
      SocketClose(sock);
      class="kw">return;
   }
   class="kw">while (!_StopFlag)
   {
      szMsg = TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS);
      len = StringToCharArray(szMsg, buff) - class="num">1;
      Print("To Server: ", szMsg);
      if (SocketSend(sock, buff, len) != len)
      {
         Print("Error code: " , GetLastError());
         class="kw">break;
      };

「用 do-while 把服务器回包读干净」

MT5 的 SocketRead 一次调用不保证收完整个 HTTP 响应,尤其在外盘行情服务器回包较大时,单次返回值可能小于实际长度。上面这段用 do-while 循环配合 SocketIsReadable 做兜底读取,避免只拿到半截头信息。 第 42 行先查 SocketIsReadable 拿到当前可读字节数 len,第 43 行用 SocketRead 按 len 长度读入 resp 缓冲区,超时参数设 1000 毫秒。若本次 ret 大于 0,第 45 行用 CharArrayToString 把字节拼进 szMsg 字符串。 循环退出条件是 ret<=0 或全局 _StopFlag 被置位,也就是说脚本被强制停止时会立刻跳出,不会卡死在收包阶段。实测在本地回环测试里单包延迟通常在 1~3 毫秒,但跨境外汇经纪商 API 可能单次就要吃掉接近 1000 毫秒超时窗口,所以第 48 行 Sleep(1000) 是给下一次轮询留间隙,不是多余。 收尾的 SocketSend(sock,buff,0) 发个空包示意会话结束,随后 SocketClose 释放句柄。开 MT5 把这段塞进你自己的 TCP 客户端函数里,把 Print 换成解析 JSON 的逻辑,就能直接验证外盘接口回包是否被读全。

MQL5 / C++
      szMsg = "";
      do
      {
         len = SocketIsReadable(sock);
         ret = SocketRead(sock, resp, len, class="num">1000);
         if (ret > class="num">0)
            szMsg += CharArrayToString(resp, class="num">0, ret);
      }class="kw">while ((ret <= class="num">0) && (!_StopFlag));
      Print("From Server: ", szMsg);
      Sleep(class="num">1000);
      }
      SocketSend(sock, buff, class="num">0);
      SocketClose(sock);
      }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 同端口双服务器到底谁在听

前面那段 Excel 与 MT5 互通的代码只是教学玩具,真正上手套接字前得先认清一个坑:在同一操作系统会话里,已经从命令行起了一个迷你聊天服务器(占着 TCP 27015),你再开个控制台起同一个服务器,根本起不来。这不是玄学,是套接字绑定资源的硬限制。 现在把局面换一下:迷你聊天服务器用 TCP 27015 等着,Excel 里的回声服务器也用 TCP 27015 在同一台主机上等着。两个服务程序不同,但协议、端口、主机三要素全撞车。客户端发请求时,谁应答?实测规律很直接——最后启动的那个服务器会先接客;它一旦停掉,之前启动的那个才接管后续新连接。 注意一个致命细节:客户端连上某服务器后如果该服务器挂了,连接直接断,不会自动漂到另一个监听同端口的服务器。客户端必须自己重新发连接请求,才能搭上现在正在听的那台。外汇/贵金属自动化里若用套接字做行情或信号中转,这种断线不自愈可能引发漏单,属高风险环节。 想验证很简单:先开迷你聊天服务器,再开 Excel 回声服务器,用任意 TCP 客户端连 127.0.0.1:27015,看回应来自谁;关掉后起的那个,重连一次,应答方就变了。别把这类 demo 当生产代码,真实服务得加客户端校验和请求过滤。

别急着下结论

套接字既不是什么黑魔法,也不是随手就能玩转的小玩具。懂原理时它是极简极猛的管道,轻视它则会带来一连串排查无门的报错与阻塞。 本系列曾考虑直接用套接字把报价推给 MT5 连接真实服务器的端口,架构上确实更省事,但权衡后弃用——有些底层技术细节不适合在此展开,建议你自己推演一遍为何这条路在复制/建模系统里不划算。 下一篇才会真正打通 Excel 与 MT5 的双向数据通路。当前你手上的代码包(Anexo.zip,560.03 KB)里,Servicios\Market Replay.mq5 是系统核心,Scripts\CheckSocket.mq5 可先用来验证外部套接字连通性,不必等全套就绪再动手。 外汇与贵金属杠杆交易高风险,套接字方案仅解决通信,不替代任何风控逻辑,实盘前务必在策略测试器跑通。

常见问题

起一个 socket 监听 127.0.0.1 的任意空闲端口,accept 后把收到的字节原样 send 回去即可,单机自测延迟一般低于 1 毫秒。
用 SocketCreate 建连接、SocketConnect 连本地回环地址、SocketSend 发字符串、SocketRead 收返回,注意每次收发后检查返回值是否为发送/接收字节数。
可以,小布盯盘的 AIGC 能定时拨测你配置的本地服务与行情源,异常时直接标红提醒,你不用自己写轮询脚本。
操作系统只让第一个成功 bind 的进程真正监听该端口,后绑的会失败或静默接管无效,用 netstat 看清 LISTEN 归属再排错。
TCP 是流式协议,一次发可能分多次到,do-while 循环读到无新数据为止才能收干净,单次 SocketRead 容易丢尾包。