开发具有 RestAPI 集成的 MQL5 强化学习代理(第 3 部分):在 MQL5 中创建自动移动和测试脚本(基础篇)
◍ 在 MQL5 里搭自动移动与回测脚本
要把强化学习代理接进 MT5,第一步不是写模型,而是先让终端能自动跑脚本、自动挪文件、自动回测。MQL5 的脚本(Script)生命周期只有一次执行,适合做这种「一次性调度」任务,而不是长期挂在图上的 EA。 实际工程里,常见做法是写一个 .mq5 脚本,用 FileCopy 把训练好的权重从沙盒目录搬到 MQL5/Files,再调用 FrameAdd 或标准库里的 Shell 执行外部回测命令。这样你每次改完代理逻辑,不用手动点终端,直接跑脚本就能批量验证。 一个可验证的落点:在 MT5 里新建脚本,编译后拖到任意图表,若文件移动成功,终端「专家日志」会打印 FileCopy 返回的代码 1(成功)或 0(失败)。这个返回值就是判断自动化链路是否通的最快信号。 外汇与贵金属品种波动受杠杆与流动性影响,自动回测结果仅代表历史样本概率,实盘存在显著高风险,参数迁移前务必在策略测试器用真实点差重跑。
从函数集成到自动落子的跨越
前面两篇已经把 MQL5 函数怎么写、以及用 FastAPI 把它和 Python 井字游戏接起来讲清楚了。这一篇不再停留于连通性验证,而是让 MQL5 端能自动计算并落子,直接抬升对局难度和交互频率。 配套会专门写一套 MQL5 测试脚本,用来在 MT5 策略测试器里反复跑集成链路,确认请求响应延迟和落子逻辑没有退化。根据井字棋状态空间规模(最多 9! = 362880 种落子顺序),自动落子算法若用极小化极大搜索,单步决策在普通笔记本上通常低于 5 毫秒。 这个项目依赖项较多,Windows 下跑 MQL5 脚本常碰到系统对外部 exe 或 DLL 的拦截。后面会给出分步安装与执行清单,并针对脚本被 UAC 或杀软限制的情况给具体绕行建议,不再像前篇那样缺这块说明。
「把自动井字代理跑通在 MT5 前的环境铺垫」
要在 MT5 里验证 MQL5 与 REST API 的通信,本地得先具备三样东西:Python 3.6 及以上、已安装的 MetaTrader 5 终端、以及 Windows 下放开脚本执行权限(必要时用管理员 PowerShell 跑 Set-ExecutionPolicy RemoteSigned)。少一项,后面的外部编译和单元测试都起不来。 项目落地分两条线。其一是把 Python 端的井字游戏 API 起好:解压后丢进 Experts 文件夹,终端进目录建虚拟环境(python -m venv env),Win 下用 env\Scripts\activate 激活,再 pip install -r requirements.txt,然后 python AppTicTacToe.py 拉起服务,浏览器开 localhost:8000/docs 就能用 Swagger UI 直接发请求。 其二是让 MetaEditor 认这个外部 Python 编译位置:菜单「工具」>「选项」>「编译器」,把虚拟环境里 script 文件夹路径贴进「外部编译位置」,编译或拖脚本上图都行。这一步是 MQL5 调用 Python 服务的物理桥梁,路径填错就只剩报错。 当前阶段的两个硬目标很具体:用智能决策算法让 Python 井字自己走子;同时在 MQL5 里写单元测试,专门盯 MQL5 代码与 REST API 交互的可靠性。外汇与贵金属交易外接脚本存在高风险,环境错配可能导致终端异常,建议先在模拟账户终端验证。 思路上并不复杂——游戏能自动应对,MQL5 脚本就能模拟真实用户去压测各种交互场景,既验游戏逻辑也验连接健壮性。持续把每次改动都过一遍单元测试,才有底气谈后续更复杂的自动决策集成。
◍ 把自动落子接进游戏内核
井字棋的自动移动不靠复杂决策树,先吃透手动逻辑再动手。原版用 self.board 存 3x3 空盘、self.player_turn 标记回合,结构极简,正是塞进 machine_move 的好底子。 machine_move 的优先级很直白:先扫空格试 'O' 能否一步连成线,能赢就落子;其次堵对手的连三;都没有就在 available_moves 返回的空位里随机选。这种写法胜率虽随随机性波动,但足以 mimic 真实对手的反应节奏。 接 FastAPI 时轮次管理是命门。玩家走完必须让接口判断 player_turn 是否已切到机器,是则立刻调 machine_move,再把更新后的棋盘与结果回写响应体。下面这段是核心类方法,逐行拆开看逻辑落点。 __init__ 建空盘并默认玩家先手;print_board 用 | 和 ----- 拼出可视格局;check_winner 扫行、列、两条对角线,非空且三格相等才返符号,否则 None;make_move 校验空格后按回合写 X 或 O 并翻转标志;machine_move 双层循环找空位,注释处留了“先找 O 的制胜点”的钩子。
def __init__(self): self.board = [[&class="macro">#x27; &class="macro">#x27; for _ in range(class="num">3)] for _ in range(class="num">3)] self.player_turn = True def print_board(self): for row in self.board: print("|".join(row)) print("-" * class="num">5) def check_winner(self): for i in range(class="num">3): if self.board[i][class="num">0] == self.board[i][class="num">1] == self.board[i][class="num">2] != &class="macro">#x27; &class="macro">#x27;: class="kw">return self.board[i][class="num">0] if self.board[class="num">0][i] == self.board[class="num">1][i] == self.board[class="num">2][i] != &class="macro">#x27; &class="macro">#x27;: class="kw">return self.board[class="num">0][i] if self.board[class="num">0][class="num">0] == self.board[class="num">1][class="num">1] == self.board[class="num">2][class="num">2] != &class="macro">#x27; &class="macro">#x27;: class="kw">return self.board[class="num">0][class="num">0] if self.board[class="num">0][class="num">2] == self.board[class="num">1][class="num">1] == self.board[class="num">2][class="num">0] != &class="macro">#x27; &class="macro">#x27;: class="kw">return self.board[class="num">0][class="num">2] class="kw">return None def make_move(self, row, col): if self.board[row][col] == &class="macro">#x27; &class="macro">#x27;: if self.player_turn: self.board[row][col] = &class="macro">#x27;X&class="macro">#x27; else: self.board[row][col] = &class="macro">#x27;O&class="macro">#x27; self.player_turn = not self.player_turn else: print("Invalid move. Try again.") def machine_move(self): for i in range(class="num">3): for j in range(class="num">3): if self.board[i][j] == &class="macro">#x27; &class="macro">#x27;: # First try to find a winning move for &class="macro">#x27;O&class="macro">#x27;
井字棋 AI 的攻防与落子逻辑
这段 Python 实现的井字棋 AI 落子逻辑,核心是先求胜、再封堵、最后随机。遍历 3x3 空位时,先假设自己落 'O',若 check_winner() 返回 'O' 就直接返回该坐标抢赢。 若没有必胜点,则假设对手落 'X',一旦 check_winner() 返回 'X' 说明此处是对手杀招,AI 立刻把该位填 'O' 并返回实现拦截。 都没匹配到就调 available_moves() 拿所有空位,用 random.choice 随机选一个落子,保证不卡死。available_moves 就是双层 for 扫 board,把值为 ' ' 的格子以 {row,col} 字典塞进列表。 TicTacToe 类里 check_winner 覆盖了 3 横、3 竖、2 斜共 8 条连线,任一全非空且相等就返回该符号。你可以把这段直接丢进 MT5 的 Python 环境或用本地解释器跑,验证拦截优先级是否如预期。
self.board[i][j] = &class="macro">#x27;O&class="macro">#x27; if self.check_winner() == &class="macro">#x27;O&class="macro">#x27;: class="kw">return (i, j) # Return position for win self.board[i][j] = &class="macro">#x27; &class="macro">#x27; # Then try to block a winning move for &class="macro">#x27;X&class="macro">#x27; self.board[i][j] = &class="macro">#x27;X&class="macro">#x27; if self.check_winner() == &class="macro">#x27;X&class="macro">#x27;: self.board[i][j] = &class="macro">#x27;O&class="macro">#x27; # Block the win of the player class="kw">return (i, j) self.board[i][j] = &class="macro">#x27; &class="macro">#x27; # If there are no winning moves, randomly choose a free position available_moves = self.available_moves() if available_moves: move = random.choice(available_moves) self.board[move["row"]][move["col"]] = &class="macro">#x27;O&class="macro">#x27; class="kw">return (move["row"], move["col"]) def available_moves(self): moves = [] for i in range(class="num">3): for j in range(class="num">3): if self.board[i][j] == &class="macro">#x27; &class="macro">#x27;: moves.append({"row": i, "col": j}) class="kw">return moves class TicTacToe: def __init__(self): self.board = [[&class="macro">#x27; &class="macro">#x27; for _ in range(class="num">3)] for _ in range(class="num">3)] self.player_turn = True def print_board(self): for row in self.board: print("|".join(row)) print("-" * class="num">5) print(f"Player {self.player_turn}&class="macro">#x27;s turn") def check_winner(self): for i in range(class="num">3): if self.board[i][class="num">0] == self.board[i][class="num">1] == self.board[i][class="num">2] != &class="macro">#x27; &class="macro">#x27;: class="kw">return self.board[i][class="num">0] if self.board[class="num">0][i] == self.board[class="num">1][i] == self.board[class="num">2][i] != &class="macro">#x27; &class="macro">#x27;: class="kw">return self.board[class="num">0][i] if self.board[class="num">0][class="num">0] == self.board[class="num">1][class="num">1] == self.board[class="num">2][class="num">2] != &class="macro">#x27; &class="macro">#x27;: class="kw">return self.board[class="num">0][class="num">0]
「机器落子的攻防与兜底逻辑」
这段实现里,机器每一步先扫描 3×3 盘面所有空格,尝试落 'O' 后调用 check_winner 判断自己能否直接连成对角线或行列,若返回 'O' 就立即占该格取胜。 紧接着它会把同一空格改填 'X' 模拟玩家落子,一旦 check_winner 返回 'X',说明这里是对手杀招,机器便抢先填 'O' 封堵,返回坐标。 若全场既无自身胜点也无对手威胁,第二段双重循环会取遍历到的第一个空格落 'O' 作为兜底,保证不跳过回合。 available_moves 则用两次 range(3) 收集所有空位坐标存进 moves 列表,返回结构为含 row 与 col 键的字典数组,方便上层直接迭代调用。
if self.board[class="num">0][class="num">2] == self.board[class="num">1][class="num">1] == self.board[class="num">2][class="num">0] != &class="macro">#x27; &class="macro">#x27;: class="kw">return self.board[class="num">0][class="num">2] class="kw">return None def machine_move(self): for i in range(class="num">3): for j in range(class="num">3): if self.board[i][j] == &class="macro">#x27; &class="macro">#x27;: self.board[i][j] = &class="macro">#x27;O&class="macro">#x27; if self.check_winner() == &class="macro">#x27;O&class="macro">#x27;: class="kw">return (i, j) self.board[i][j] = &class="macro">#x27; &class="macro">#x27; self.board[i][j] = &class="macro">#x27;X&class="macro">#x27; if self.check_winner() == &class="macro">#x27;X&class="macro">#x27;: self.board[i][j] = &class="macro">#x27;O&class="macro">#x27; class="kw">return (i, j) self.board[i][j] = &class="macro">#x27; &class="macro">#x27; for i in range(class="num">3): for j in range(class="num">3): if self.board[i][j] == &class="macro">#x27; &class="macro">#x27;: self.board[i][j] = &class="macro">#x27;O&class="macro">#x27; class="kw">return (i, j) def available_moves(self): moves = [] for i in range(class="num">3): for j in range(class="num">3): if self.board[i][j] == &class="macro">#x27; &class="macro">#x27;: moves.append({"row": i, "col": j}) class="kw">return moves