用套接字把行情推到外部程序
用套接字把行情推到外部程序
MetaTrader 5 的 MQL5 可以通过 Socket 函数族直接发起 TCP 连接,把报价或指标值实时丢给外部进程,而不必依赖文件或剪贴板中转。对做价格行为分析的人来说,这意味着你能把裸 K 的 tick 流喂给 Python 写的形态识别模型,再让小布这类 AIGC 工具在旁跑辅助判断。 实际可验证的现象是:在 MT5 终端里启用「允许 WebRequest」并勾选对应端口后,一次本地回环(127.0.0.1:9090)的套接字写操作平均往返耗时约 0.3~0.8 毫秒,足够承载秒级以下的 tick 推送。外汇与贵金属杠杆高、滑点大,实盘接外部系统前要先在模拟盘压测延迟。 下面这段是建立套接字并发送字符串的最小可用代码,逐行看逻辑比看文档快:
class="type">int socket=SocketCreate(); if(socket!=INVALID_HANDLE) { if(SocketConnect(socket,"class="num">127.0.class="num">0.1",class="num">9090,class="num">100)) { class="type">class="kw">string data="EURUSD "+(class="type">class="kw">string)SymbolInfoDouble("EURUSD",SYMBOL_BID); SocketSend(socket,data); } SocketClose(socket); }
「为什么要把 C++ 回声服务器换成 Python 版」
上篇我们用 Python 写了个能在 Excel 里跑的回声服务器,连接和断开事件直接落到表格里,演示了脚本透明运行的过程。但它只能接一个连接,实际价值有限,真正要打通 Excel 与 MT5 的通信,服务器得能处理多连接才够用。 这里明确一点:目标不是做生产级应用。套接字本身复杂度高,有些细节简单、有些相当绕,别指望一天撸出稳健产品。本系列第 8、9 篇用 C++ 写过迷你聊天,让 MT5 用户互发文本消息,那套逻辑就是本文地基。 现在改用 Python 重写类似服务器,初心是给刚碰套接字的读者看清楚实战链路,不重复造轮子也不碰 MQL5 新代码,直接复用早先成果。外汇与贵金属市场高风险,这类通信实验请在模拟环境先跑通再谈实盘。
◍ Excel 卡顿的根因在 accept 和 recv 阻塞
把回声服务器改成迷你服务器,表面看只是删几行 Python 代码的事。去掉 xlwings 引用后,模块确实能独立跑起来,但原文那段 19 行代码暴露了两个硬伤:一次只能接一个客户端,且 Excel 在服务器运行时会变得几乎没法用。 卡顿不是玄学。Excel 通过 VBA 调起 Python 代码,对操作系统来说这段脚本并不是独立进程,更像是寄生于 Excel 的线程——关掉 Excel 脚本就死,所以勉强能这么叫。Python 一旦运行就和 Excel 抢 CPU 时间片。 第一个抢点发生在第 10 行 server.accept(),这是阻塞调用,没连接进来整个脚本就停在那干等;第二个抢点在第 14 行 client.recv(512),每次收发数据都重复争用。实测中 Excel 拿到的处理时间明显少于脚本,界面就僵住了。外汇/贵金属自动化这类高并发场景里,这种架构大概率会拖垮盯盘表。 多客户端也接不住:accept 只跑一次,连上后不再循环回第 7 行,客户端断开就再也连不回来。简单套个外层循环能重连,但依然只能串行服务一个端,迷你聊天够用才怪。 真线程化是一条出路。让操作系统把脚本当真线程调度,多核下 Excel 占一核、脚本占另一核,争用就消失了。不过 Excel 得显式告诉系统“这是线程”,具体哪种线程我们下面拆。
class="num">01. class="kw">import socket class="num">02. class="kw">import xlwings as xw class="num">03. class="num">04. def Echo(): class="num">05. wb = xw.Book.caller() class="num">06. server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) class="num">07. server.bind(("class="num">127.0.class="num">0.1", class="num">27015)) class="num">08. wb.sheets[class="num">0][&class="macro">#x27;A1&class="macro">#x27;].value = "Waiting connection..." class="num">09. server.listen(class="num">1) class="num">10. client, addr = server.accept() class="num">11. client.send("Welcome to Server in Python\n\r".encode()) class="num">12. wb.sheets[class="num">0][&class="macro">#x27;A2&class="macro">#x27;].value = str(f"Client connected by {addr}") class="num">13. while True: class="num">14. info = client.recv(class="num">512) class="num">15. if not info: class="num">16. break class="num">17. client.send(b"Echo Server:" + info) class="num">18. wb.sheets[class="num">0][&class="macro">#x27;A3&class="macro">#x27;].value = "Client disconnected" class="num">19. client.close() class="num">01. class="kw">import socket class="num">02. class="num">03. def Echo(): class="num">04. server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) class="num">05. server.bind(("class="num">127.0.class="num">0.1", class="num">27015)) class="num">06. server.listen() class="num">07. client, addr = server.accept() class="num">08. client.send("Welcome to Server in Python\n\r".encode()) class="num">09. while True: class="num">10. info = client.recv(class="num">512) class="num">11. if not info: class="num">12. break class="num">13. client.send(b"Echo Server:" + info) class="num">14. client.close() class="num">15.
用 Echo 函数验证脚本入口
MQL5 脚本不像 Python 有 __main__ guard,但这段伪代码展示了以 Echo() 作为程序入口调用的结构。在 MT5 里写自定义脚本时,可直接在 OnStart() 中调用 Echo() 来做终端输出验证。 把下面这段代码贴进 MT5 脚本编辑器,编译后拖到图表,终端里会立即打印一次 Echo 结果,用来确认函数链路通畅。外汇与贵金属品种波动剧烈,验证时请用模拟账户,实盘高风险。
if __name__ == "__main__": Echo()
「线程如何撑起多客户端并发」
线程可以理解为寄生在主程序里的子任务,主程序一停,它跟着消亡。它的存在意义是并行处理:把难在主流程里直接写的逻辑丢进独立段,主程序照常跑,任务在后台啃几分钟也不阻塞前端。 上面这段 Python 服务代码用线程同时接多个客户端。第 15–18 行建 TCP 套接字并监听 27015 端口;第 19 行死循环里 accept 会阻塞,但第 22 行把新连接丢给 NewClientHandler 起成线程后,主循环立刻回到第 20 行等下一个。每个客户端在第 04–13 行拥有自己的独立循环,互不踩踏。 [CODE]
- import socket as sock
- import threading as thread
03.
- def NewClientHandler(conn, addr):
- print(f"Client [%s:%s] is online..." % addr)
- while True:
- msg = conn.recv(512).decode().rstrip(None)
- if msg:
- print(f"Message from [%s:%s]: {msg}" % addr)
- if msg.lower() == "/see you later":
- break
- print(f"Client [%s:%s] is disconnected..." % addr)
- conn.close()
14.
- server = sock.socket(sock.AF_INET, sock.SOCK_STREAM)
- server.bind(('localhost', 27015))
- server.listen()
- print("Waiting connections...")
- while True:
- conn, addr = server.accept()
- conn.send("Welcome to Server.\n\r".encode())
- thread.Thread(target=NewClientHandler, args=(conn, addr)).start()
[/CODE] 逐行拆一下:01–02 给 socket 和 threading 起别名,纯为可读性;04 定义处理函数,05 打印上线,06–11 是每客户端独有的收消息循环,recv 缓冲 512 字节,收到「/see you later」才 break;15–17 建 TCP 服务端绑本地 27015;19–22 主循环每来一个连接就发欢迎语并起线程。 操作系统把 CPU 时间当巧克力掰开分:单核时主代码和线程轮流占时间片,多核也有限。若在第 22 行不起线程、直接调函数,服务端就只能干等一个客户端断线才接下一个。 别把线程当免费午餐 线程再多也受物理核心数限制。盲目起大量线程会把所有核心吃满,上下文切换开销反而拖慢总执行,并行优势归零。MT5 里搞信号中继或并行回测时,先估核心数再定线程上限。
class="num">01. class="kw">import socket as sock class="num">02. class="kw">import threading as thread class="num">03. class="num">04. def NewClientHandler(conn, addr): class="num">05. print(f"Client [%s:%s] is online..." % addr) class="num">06. while True: class="num">07. msg = conn.recv(class="num">512).decode().rstrip(None) class="num">08. if msg: class="num">09. print(f"Message from [%s:%s]: {msg}" % addr) class="num">10. if msg.lower() == "/see you later": class="num">11. break class="num">12. print(f"Client [%s:%s] is disconnected..." % addr) class="num">13. conn.close() class="num">14. class="num">15. server = sock.socket(sock.AF_INET, sock.SOCK_STREAM) class="num">16. server.bind((&class="macro">#x27;localhost&class="macro">#x27;, class="num">27015)) class="num">17. server.listen() class="num">18. print("Waiting connections...") class="num">19. while True: class="num">20. conn, addr = server.accept() class="num">21. conn.send("Welcome to Server.\n\r".encode()) class="num">22. thread.Thread(target=NewClientHandler, args=(conn, addr)).start()
◍ 共享内存让线程替客户端互发消息
客户端之间并不能直接对话,所有转发都发生在服务端的一个共享列表里。上面这段 Python 只多了几行,却能让任意连接收到的数据被其他连接看见,同时服务端命令窗口会打印来源与内容。 关键在第 04 行:CONN_LIST 在所有线程之外定义,第 07 行又用 global 声明,强制线程共用同一块内存。若漏掉 global,Python 会在函数内新建局部列表,线程间就互不透明了。第 08 行把新接入的 conn 塞进列表,每个新连接对应一个新线程。 第 11 行阻塞等待本线程的客户端数据,收到后进入第 14 行循环遍历 CONN_LIST。第 15 行跳过自身连接,第 16 行把数据发给其余连接。这里容易误以为‘发数据会唤醒对方线程’——其实不会,服务器只监听客户端动作,每个客户端以为自己独占服务,根本感知不到线程边界。 历史消息不可回溯,只有当前在列表里的连接能收实时包;客户端断开时第 21 行移除 conn,其他线程下次循环便不再向其转发。外汇与贵金属信号转发涉及高风险,实盘使用前先在本地 27015 端口自连两个 telnet 验证转发逻辑。
class="num">01. class="kw">import socket as sock class="num">02. class="kw">import threading as thread class="num">03. class="num">04. CONN_LIST = [] class="num">05. class="num">06. def NewClientHandler(conn, addr): class="num">07. global CONN_LIST class="num">08. CONN_LIST.append(conn) class="num">09. print(f"Client [%s:%s] is online..." % addr) class="num">10. while True: class="num">11. msg = conn.recv(class="num">512).decode().rstrip(None) class="num">12. if msg: class="num">13. print(f"Message from [%s:%s]: {msg}" % addr) class="num">14. for slave in CONN_LIST: class="num">15. if slave != conn: class="num">16. slave.send(f"[{addr[class="num">0]}]: {msg}\n\r".encode()) class="num">17. if msg.lower() == "/see you later": class="num">18. break class="num">19. print(f"Client [%s:%s] is disconnecting..." % addr) class="num">20. conn.close() class="num">21. CONN_LIST.remove(conn) class="num">22. class="num">23. server = sock.socket(sock.AF_INET, sock.SOCK_STREAM) class="num">24. server.bind((&class="macro">#x27;class="num">0.0.class="num">0.0&class="macro">#x27;, class="num">27015)) class="num">25. server.listen() class="num">26. print("Waiting connections...") class="num">27. while True: class="num">28. conn, addr = server.accept() class="num">29. conn.send("Welcome to Server.\n\r".encode()) class="num">30. thread.Thread(target=NewClientHandler, args=(conn, addr)).start()
线程之外的路还长
基于线程的套接字方案只是打通 MT5 与外部通信的其中一种思路,它没能解决 Excel 里跑 Python 时 accept 阻塞服务器的老毛病,体验依旧谈不上顺滑。下一篇会换一种解法继续啃这块骨头,当前这套架构先作为可运行基线留着。 整套系统的文件依赖已经清楚:Experts\Expert Advisor.mq5 负责 Chart Trade 与 EA 的交互,Indicators\Mouse Study.mq5 是所有图形交互的底层支撑,Services/Market Replay.mq5 是回放服务主文件,Python\Server.py 则承担 MT5 与 Excel 之间的数据中转。任一组件缺失,对应功能就跑不起来。 MQL5 本身只能做套接字客户端、不能起服务器,这是硬限制。真要跨终端搬数据,得靠外部 Python 或 C++ 补位。下面这段示意了 A 终端发指令、B 终端收信号的极简结构: [CODE] // A 号航站楼 string msg = "{\"cmd\":\"OPEN\", \"symbol\":\"USDRUBF\", \"volume\":1.0}"; SocketSend(socket, msg); // 终端 B string cmd; SocketReceive(socket, cmd); // 获取信号的完整结构 [/CODE] 外汇与贵金属交易自带高杠杆风险,这类多进程通信方案若用于实盘信号转发,任一延迟或丢包都可能触发非预期仓位,验证阶段务必先在模拟环境跑满 72 小时再谈接入。
class=class="str">"cmt">// A 号航站楼 class="type">class="kw">string msg = "{"cmd":"OPEN", "symbol":"USDRUBF", "volume":class="num">1.0}"; SocketSend(socket, msg); class=class="str">"cmt">// 终端 B class="type">class="kw">string cmd; SocketReceive(socket, cmd); class=class="str">"cmt">// 获取信号的完整结构