开发具有 RestAPI 集成的 MQL5 强化学习代理(第 3 部分):在 MQL5 中创建自动移动和测试脚本·进阶篇
(2/3)· 从 REST API 打通到 MQL5 自动移动与测试脚本,跨语言集成的坑都在这一步
很多交易者在把 Python 训练好的模型接回 MQL5 时,卡在脚本权限和环境隔离上,却误以为是接口写错。本篇承接上篇的 FastAPI 集成,直接给出自动落子与测试脚本的可执行路径,跳过这些隐性门槛能省下数小时调试。
「让 API 接管机器的那一步落子」
把井字游戏做成人机持续对弈,核心在 play 接口:玩家每推一步,服务端先落子再判断是否轮到机器,机器逻辑被激活后才回写自己的棋步。这样人类和 AI 在同一局里交替走,状态接口会回传更新后的棋盘、双方棋步与胜负判定。 实际部署里,FastAPI 的 POST 路由接收 game_id 与玩家坐标,先查局是否存在(404),再验目标格是否为空(否则 400)。玩家标 'X',机器标 'O',谁赢由 check_winner 决定,未分胜负且轮到机器时自动补一步。 返回体里 available_moves 很关键——前端能用它实时灰掉不可落子的格子,把轮次锁死,避免重复提交造成的状态漂移。 下面这段就是 play 路由的真实骨架,逐行看逻辑比读文档快: @app.post("/play/{game_id}/") 声明接收游戏编号的落子请求 def play(game_id: int, move: PlayerMove): 入参含游戏 ID 与玩家走法对象 game = games.get(game_id) 从内存局表取对局 if not game: 局不存在直接抛 404 raise HTTPException(status_code=404, detail="Game not found") board = game.board 取当前棋盘二维数组 if board[move.row][move.col] == ' ': 目标格为空才允许落子 board[move.row][move.col] = 'X' 玩家落 X else: 否则报 400 非法走法 raise HTTPException(status_code=400, detail="Invalid move") player_move = {"row": move.row, "col": move.col, "symbol": 'X'} 记录玩家这一步 winner = game.check_winner() 先判玩家是否已胜 machine_move_result = None 初始化机器回写 if not winner: 没结束才轮到机器 game.player_turn = not game.player_turn 交权 if not game.player_turn: 确为机器回合 move_result = game.machine_move() 调自动走法 if move_result: 有返回才写结果 row, col = move_result machine_move_result = {"row": row, "col": col, "symbol": 'O'} 机器落 O winner = game.check_winner() 再判胜负 game.player_turn = not game.player_turn 交还人类 return { 回传完整状态 "board": board, 最新棋盘 "player_move": player_move, 玩家步 "machine_move": machine_move_result, 机器步或空 "winner": winner, 胜方或 None "available_moves": game.available_moves() 可落子坐标 }
@app.post("/play/{game_id}/") def play(game_id: class="type">int, move: PlayerMove): game = games.get(game_id) if not game: raise HTTPException(status_code=class="num">404, detail="Game not found") board = game.board if board[move.row][move.col] == &class="macro">#x27; &class="macro">#x27;: board[move.row][move.col] = &class="macro">#x27;X&class="macro">#x27; else: raise HTTPException(status_code=class="num">400, detail="Invalid move") player_move = {"row": move.row, "col": move.col, "symbol": &class="macro">#x27;X&class="macro">#x27;} winner = game.check_winner() machine_move_result = None if not winner: game.player_turn = not game.player_turn if not game.player_turn: move_result = game.machine_move() if move_result: row, col = move_result machine_move_result = {"row": row, "col": col, "symbol": &class="macro">#x27;O&class="macro">#x27;} winner = game.check_winner() game.player_turn = not game.player_turn class="kw">return { "board": board, "player_move": player_move, "machine_move": machine_move_result, "winner": winner, "available_moves": game.available_moves() }
◍ 把 REST 交互拆成可跑的断言用例
在 MT5 里验证 HTTP 交互不能靠肉眼看日志,最稳的做法是把每个接口场景写成独立测试函数,用 Assert 拦住异常返回。一套基础结构通常拆成三个文件:Tests.mqh 放测试逻辑,Request.mqh 管通信,Tests.mq5 作为入口脚本一次性拉起全部用例。 断言本身是极简的——条件为假才打印,真就静默通过,这样跑完一长串测试你只看得到报错项。下面这段是核心断言与初始化用例的原样代码,逐行拆完你就能照抄。 void Assert(bool condition, const string message) 定义断言函数,入参是布尔条件和报错文本。 if(!condition) 当条件不成立时进入分支。 Print("Test error: ", message) 向 MT5 终端打印带前缀的错误说明。 void TestGameInitialization() 定义游戏初始化测试。 string url 写死本地 8000 端口的 start-game 路由。 int result = Request("GET", response, url) 发 GET 并拿回状态码。 Assert(result == 200, ...) 期望 HTTP 200,否则报初始化失败。 Assert(StringLen(response) > 0, ...) 确认响应体里有 game_id 字段返回。 真实落盘时,TestPlayerMove 会先 GET 拿 game_id,再用 POST 带 {"row":0,"col":0} 落子,断言 200 且响应含 player_move 字样;TestInvalidPlayerMove 在同一格重复 POST,期望 API 回 400。 旧版 Request 库没有调试开关,排错只能手动 Print。新版 SendGetRequest / SendPostRequest 加了 debug 选项,打开后自动吐出请求与响应明细,错误处理也从单纯返码改成可定位的诊断信息——接 REST 做策略信号的人,建议直接拿这版库改。 外汇与贵金属行情受消息面驱动,API 联调仅解决技术通路,实盘仍属高风险,参数上线前务必在模拟环境跑通全部断言。
class="type">void Assert(class="type">bool condition, const class="type">class="kw">string message) { if(!condition) { Print("Test error: ", message); } } class="type">void TestGameInitialization() { class="type">class="kw">string url = "http:class=class="str">"cmt">//localhost:class="num">8000/start-game/"; class="type">class="kw">string response; class="type">int result = Request("GET", response, url); Assert(result == class="num">200, "Game initialization failed"); Assert(StringLen(response) > class="num">0, "game_id missing in game initialization response"); }
用 HTTP 请求驱动井字棋对局验证
这段 MQL5 片段演示了如何通过本地 REST 接口自动跑一局井字棋,并断言玩家 X 获胜。它先取回 game_id,再连续发 POST 落子,每一步都要求服务器回 200,否则测试直接报错。 落子坐标按 (0,0)、(0,2)、(2,2)、(1,2) 顺序推送,第四手后服务端状态里 winner 字段应等于 "X",断言同时卡了 result==200 与 js["winner"].ToStr()=="X" 两个条件。 RunTests 把初始化、合法落子、非法落子、胜利判定四个用例串起来,在 MT5 策略测试器里跑能直接看到哪一手断链。外汇与贵金属交易同理——任何自动逻辑上线前都该有这种可断言的回放,市场高风险,验证不充分可能放大亏损。
class="type">int result = -class="num">1; class="type">int game_id = -class="num">1; Request("GET", response, url); js.Deserialize(response); game_id = js["game_id"].ToStr(); class=class="str">"cmt">// Make moves for player X to win url = StringFormat("http:class=class="str">"cmt">//localhost:class="num">8000/play/%d/", game_id); class="type">class="kw">string payload = "{\"row\": class="num">0, \"col\": class="num">0}"; result = Request("POST", response, url, payload); Assert(result == class="num">200, "Player X move class="num">1 failed"); payload = "{\"row\": class="num">0, \"col\": class="num">2}"; result = Request("POST", response, url, payload); Assert(result == class="num">200, "Player X move class="num">2 failed"); payload = "{\"row\": class="num">2, \"col\": class="num">2}"; result = Request("POST", response, url, payload); Assert(result == class="num">200, "Player X move class="num">3 failed"); payload = "{\"row\": class="num">1, \"col\": class="num">2}"; result = Request("POST", response, url, payload); Assert(result == class="num">200, "Player X move class="num">4 failed"); class=class="str">"cmt">// Check if the response contains information about the winner js.Deserialize(response); class=class="str">"cmt">// Deserialize the updated game state class=class="str">"cmt">// Check if the HTTP response code is class="num">200 (OK) after move class="num">5 Assert(result == class="num">200 && js["winner"].ToStr() == "X", "Player X victory failed"); } class=class="str">"cmt">// Function to run all tests class="type">void RunTests() { TestGameInitialization(); TestPlayerMove(); TestInvalidPlayerMove(); TestPlayerWin(); }
「给请求库动刀的两个实在理由」
改 Requests 库不是为炫技,核心就两条:调试可控、错误兜得住。 把调试开关做成可开可关,意味着你在 MT5 里跑 EA 调 HTTP 接口时,能随时把 API 原始响应 dump 出来看,不用为了查一个字段错位去翻日志文件翻半天。 网络层永远比本地计算更不可控。对外汇和贵金属信号中转这类依赖外部 API 的 EA,一个超时或 500 错误若没被单独 catch,可能直接让持仓逻辑崩掉——改进错误管理就是让这类异常变成可记录、可重试的事件,而不是静默失败。 这两处改动不直接产生收益,但能让你在实盘前用策略测试器反复验证接口健壮性,把通信故障的概率压下去。
◍ 调试开关与错误处理的实际收益
给请求函数加一个 debug 布尔参数,是这次改动里性价比最高的一笔。旧版无论成功失败都闷头返回,出问题只能靠猜;新版在 debug=true 时才用 Print 把响应体打到日志,开发阶段能少走很多弯路。 错误处理那块,旧实现里只要 HTTP 层返回非 -1,就立刻 Print 出完整报文并返回 ERR_HTTP_ERROR_FIRST+res。这意味着哪怕你只想静默重试,终端也会被刷屏。新实现把 Print 收进 if(debug) 判断,正常行情跑 EA 时日志干净,只在你主动开调试时才吐数据。 下面这段对比能直接抄进 MT5 的 include 里验证:旧函数固定返回 0 吞掉状态码,新版返回 res 真实值,配合 debug 开关定位 API 联通性问题的效率会明显提升。外汇与贵金属接口调用受网络波动影响大,这类改动只能降低调试成本,不保证请求成功率。
class=class="str">"cmt">// Example of old SendGetRequest implementation class="type">int SendGetRequest(const class="type">class="kw">string url, const class="type">class="kw">string query_param, class="type">class="kw">string &out, class="type">class="kw">string headers = "", const class="type">int timeout = class="num">5000) { class=class="str">"cmt">// ... code ... out = CharArrayToString(result, start_index, WHOLE_ARRAY, CP_UTF8); class="kw">return (class="num">0); } class=class="str">"cmt">// Example of new SendGetRequest implementation class="type">int SendGetRequest(const class="type">class="kw">string url, const class="type">class="kw">string query_param, class="type">class="kw">string &out, class="type">class="kw">string headers = "", const class="type">int timeout = class="num">5000, class="type">bool debug=false) { class=class="str">"cmt">// ... code ... out = CharArrayToString(result, start_index, WHOLE_ARRAY, CP_UTF8); if(debug) { Print(out); } class="kw">return res; } class=class="str">"cmt">// Example of old implementation of SendGetRequest class="type">int SendGetRequest(const class="type">class="kw">string url, const class="type">class="kw">string query_param, class="type">class="kw">string &out, class="type">class="kw">string headers = "", const class="type">int timeout = class="num">5000) { class=class="str">"cmt">// ... code ... if(res == -class="num">1) { class="kw">return (_LastError); } else { class=class="str">"cmt">// HTTP error handling out = CharArrayToString(result, class="num">0, WHOLE_ARRAY, CP_UTF8); Print(out); class="kw">return (ERR_HTTP_ERROR_FIRST + res); } } class=class="str">"cmt">// Example if new implementation of SendGetRequest class="type">int SendGetRequest(const class="type">class="kw">string url, const class="type">class="kw">string query_param, class="type">class="kw">string &out, class="type">class="kw">string headers = "", const class="type">int timeout = class="num">5000, class="type">bool debug=false) { class=class="str">"cmt">// ... code ... if(res == -class="num">1) { class="kw">return (_LastError); } else { class=class="str">"cmt">// HTTP error handling out = CharArrayToString(result, class="num">0, WHOLE_ARRAY, CP_UTF8); if(debug) { Print(out); } class="kw">return res; } }
把改动接进自动对弈做实测
把 Requests 库的改动接进 Python 井字棋的自动走子逻辑,是验证整套通信层是否靠谱的唯一办法。只改库不跑实局,永远不知道超时、重发和解析在连续对弈里会不会互相打架。 实测时建议让两边脚本各跑 50 局以上,记录平均单步响应耗时与异常中断次数。若中断率超过 2% 或单步耗时中位数高于 300ms,说明集成层仍有阻塞点,需要回退到上一步查连接池配置。 外汇与贵金属自动化同理属于高风险操作,任何通信层改动在接真仓前都必须用模拟环境跑满样本量再谈下一步。