Skill: apuba/mt4-trader (version 1.2.1)

# MT4 智能交易桥 · 量化自动交易系统 MT4 智能交易桥。通过文件 I/O 与 MT4 EA 通信，无需 DLL 或额外依赖。支持 OpenClaw（小龙虾 AI Agent）通过微信 / 飞书 / Telegram 等渠道直接操作 MT4 交易。 > **关键词**：MT4, MetaTrader 4, 外汇交易, Python交易, 算法交易, 自动交易, 网格策略, 支撑压力位, ATR止损, 移动止损, 黄金交易, 原油交易, 比特币交易, OpenClaw, 小龙虾, AI交易助手, 微信操作MT4, 飞书操作MT4 ## 🎯 自然语言交互示例 ### 📱 微信/飞书/Telegram 口语化命令 | 你说 | MT4 做 | |-----|--------| | 黄金多少钱 | 返回 XAUUSD.s 实时报价 | | 买黄金 0.05 手 | 市价买入 0.05 手 | | 平仓 95832104 | 平掉指定订单 | | 全平 | 平掉所有持仓 | | 查持仓 | 列出当前所有持仓 | | 设置止损 95832104 4500 | 设置止损位 | | 启动黄金网格 | 启动黄金网格策略 | | 获取MT4 黄金的1小时K线数据进行行情分析 | 将会自动帮您分析好黄金后市走向以及给出交易方案 | --- ## 文件结构 ``` mt4-trader/ ├── SKILL.md # 本使用说明 ├── mql4/ # MT4 EA 编译文件（.ex4，由脚本生成） │ [用户手动下载 .ex4 文件放到此目录] ├── scripts/ # Python 脚本 │ ├── mt4_client.py # Python 交易客户端（核心） │ ├── deploy_ex4.py # EA 部署指引（显示下载链接和安装步骤） │ ├── alert_commands.py # 价格预警命令行工具 │ ├── position_summary.py # 持仓汇总（命令行输出） │ ├── position_summary_cron.py # 持仓汇总（定时推送） │ ├── sr_strategy.py # 支撑压力位策略编排 │ └── summary_utils.py # 持仓汇总工具模块 └── references/ └── api_docs.md # 完整 API 参考文档 ``` --- ## 安装步骤（详细版） ### 第一步：找到你的 MT4 数据目录 每个 MT4 安装实例都有一个专属的数据目录，格式为： ``` %APPDATA%\MetaQuotes\Terminal\<32位随机ID> ``` 例如：`C:\Users\Administrator\AppData\Roaming\MetaQuotes\Terminal\ABC123...` > 怎么找？在 MT4 中按 `F7` 打开 MetaEditor（代码编辑器），点击菜单 `文件 → 打开数据文件夹`，弹出的路径就是你的 MT4 数据目录。 ### 第二步：确认 MT4 安装目录 EA 文件需要部署到两个不同的目录： | EA 文件 | 目标目录 | 说明 | |---------|---------|------| | `mt4_bridge.ex4` | `MT4安装目录\MQL4\Experts\` | 主程序 EA（必须） | | `芝麻网格V2.ex4` | `MT4安装目录\MQL4\Experts\` | 网格策略 EA（可选） | | `tools2.3.ex4` | `MT4安装目录\MQL4\Libraries\` | **网格策略依赖库**（网格必须，不是 Experts！） | > ⚠️ `Libraries` 目录与 `Experts`、`Indicators` 同级，**不是** `Experts` 的子目录。 典型路径结构： ``` C:\Program Files (x86)\EBC Financial Group Cayman MT4 Terminal\MQL4\ ├── Experts\ ← mt4_bridge.ex4, 芝麻网格V2.ex4 放这里 │ ├── mt4_bridge.ex4 │ └── 芝麻网格V2.ex4 ├── Libraries\ ← tools2.3.ex4 放这里（与 Experts 同级！） │ └── tools2.3.ex4 ├── Indicators\ └── Scripts\ ``` ### 第三步：下载 .ex4 文件 从 Gitee 仓库下载 3 个 .ex4 文件： https://gitee.com/3603317/skill-plugin/tree/master/mt4 需要下载的文件： | 文件 | 说明 | |------|------| | `mt4_bridge.ex4` | 主程序 EA（**必选**） | | `芝麻网格V2.ex4` | 网格策略 EA（可选） | | `tools2.3.ex4` | 网格策略依赖库（选网格则必选） | > 点击文件 → 点右上角 **Raw** 按钮即可保存到本地。 下载后放入 skills\mt4-trader\mql4\ 目录，然后运行以下命令一键部署到 MT4： ```bash cd scripts\ python deploy_ex4.py deploy ``` ### 第四步：在 MT4 中挂载 EA **挂载 mt4_bridge（核心，必须操作）：** 1. 打开 MT4，按 `Ctrl+N` 打开左侧"导航器"面板 2. 展开"EA 交易"列表，找到 `mt4_bridge` 3. 用鼠标左键拖拽到任意品种图表上（推荐 XAUUSD.s 或主流品种） 4. 弹出 EA 属性对话框 → **选项卡"常用"**： - **允许实时交易** → ✅ 勾选（必须！） - **允许导入 DLL** → ✅ 勾选 - **确认取消"手动交易禁止"** 等限制 5. 点"确定"关闭对话框 6. 图表右上角显示一个 EA 图标： - 😊 **绿色笑脸** → ✅ 正常工作中 - 😢 哭脸或灰脸 → 检查下面"第五步"的自动交易按钮 7. **右键图表 → 属性 → 常用** 中可修改 EA 输入参数（可选） **可选：挂载芝麻网格 EA：** 1. 将 `芝麻网格V2` EA 拖到需要跑网格的品种图表上（如 XAUUSD.s、XTIUSD.s） 2. 勾选"允许实时交易" 3. **确认 `tools2.3.ex4` 已放在 `Libraries\` 目录中**，否则网格 EA 无法运行 **多图表挂载（重要）：** - mt4_bridge EA 可以挂载在**多个品种**的图表上（例如黄金一个图表、原油一个图表） - 每个 EA 实例自动通过品种专属请求文件区分，不会互相干扰 - Python 客户端传了 `symbol` 参数后，会自动路由到对应的品种请求文件 ### 第五步：开启自动交易 这是新手最容易忽略的一步！ 1. 在 MT4 工具栏上方找到 **"自动交易"按钮**（一个黄色播放 ▶️ 图标） 2. 如果它是 **灰色** 的 → 点击一下，变成 **绿色** ✅ 3. 此时所有已挂载的 EA 图标都应变成 **绿色笑脸** 😊 4. 如果某个 EA 还是哭脸，右键该 EA 图标 → "属性" → 勾选"允许实时交易" > 💡 每次重启 MT4 后，自动交易按钮默认为关闭状态，需要手动开启。 > 建议在 MT4 设置中勾选"启动时启用自动交易"。 ### 第六步：验证部署是否成功 **方法一：脚本检查** ```bash cd scripts\ python mt4_client.py ``` 如果看到类似下面的交互菜单，说明通信正常： ``` === MT4 交易助手 === 1. 获取报价 2. 查询持仓 3. 交易 4. 网格策略 请输入指令： ``` **方法二：Python 代码验证** ```python from mt4_client import MT4Client client = MT4Client() # 获取账户信息（确认通信正常） info = client.get_account_info() print(info) # 应该看到余额、净值、可用保证金 # 获取实时报价 price = client.get_price("XAUUSD.s") print(price) # 应该看到 bid/ask 价格 ``` **如果返回 `"MT4 超时未响应"`：** - MT4 在运行吗？→ 打开 MT4 - EA 挂载了吗？→ 图表上有没有 EA 图标 - 自动交易开了吗？→ 工具栏自动交易按钮是绿色吗 - EA 属性中"允许实时交易"勾了吗？→ 右键 EA 图标检查 --- ## 核心 Python 方法 ```python from mt4_client import MT4Client client = MT4Client() # === 行情 === client.get_price("XAUUSD.s") # 获取实时报价（bid/ask） client.get_positions() # 查询持仓列表 client.get_account_info() # 账户余额/净值/保证金 client.get_history(7) # 历史订单（最近 7 天） # === 交易 === client.buy(symbol="XAUUSD.s", lots=0.03) # 市价买入 0.03 手 client.sell(symbol="XAUUSD.s", lots=0.03) # 市价卖出 0.03 手 client.close(ticket=123456) # 平指定订单 client.close_all() # 一键全平 client.partial_close(ticket=123456, lots=0.02) # 部分平仓 # === 止损止盈 === client.set_sl(ticket=123456, sl=75000) # 设置止损 client.set_tp(ticket=123456, tp=80000) # 设置止盈 client.modify_order(ticket=123456, sl=75000, tp=80000) # 同时修改 client.auto_sl(ticket=123456, risk_percent=2.0) # 按账户 2% 风险自动止损 client.atr_sl(ticket=123456, period=14, multiplier=2.0) # ATR 止损 client.trailing_stop(ticket=123456, distance=300) # 移动止损（300 点间距） # === 平所有多单/空单 === client.close_all_buy() # 平所有多单 client.close_all_sell() # 平所有空单 client.close_profit() # 平所有盈利单 client.close_loss() # 平所有亏损单 # === 挂单 === client.buy_limit(symbol="XAUUSD.s", lots=0.03, price=4500.0) # 限价买入 client.sell_limit(symbol="XAUUSD.s", lots=0.03, price=4600.0) # 限价卖出 client.buy_stop(symbol="XAUUSD.s", lots=0.03, price=4700.0) # 买入止损（突破追涨） client.sell_stop(symbol="XAUUSD.s", lots=0.03, price=4500.0) # 卖出止损（破位追空） client.get_pending_orders() # 查询当前挂单 client.cancel_pending(ticket=123456) # 取消挂单 # === OCO 订单（二选一挂单） === client.oco_order(symbol="XAUUSD.s", lots=0.03, order1={"type": "buy_stop", "price": 77000, "sl": 76500}, order2={"type": "sell_stop", "price": 76000, "sl": 76500} ) # === 手数计算 === client.calc_lots(symbol="XAUUSD.s", risk_percent=1.0, sl_distance=5.0) # 1% 风险，止损 5 美元 → 返回建议手数 # === K 线数据（量化分析用） === client.get_klines(symbol="XAUUSD.s", timeframe="H1", count=20) # H1 最近 20 根 client.get_klines(symbol="XAUUSD.s", timeframe="M15", count=100) # M15 最近 100 根 client.get_klines(symbol="XAUUSD.s", timeframe="D1", count=30) # 最近 30 根日线 # === 支撑压力位 === client.get_sr_levels(symbol="XAUUSD.s") # 获取支撑压力位 client.draw_sr_levels(symbol="XAUUSD.s") # 在图表上画出支撑压力位线 client.clear_chart_objects(symbol="XAUUSD.s") # 清除图表上所有对象 client.draw_hline(symbol="XAUUSD.s", name="R1", price=4750) # 画自定义水平线 client.clear_hline(symbol="XAUUSD.s", prefix="HL_") # 按前缀删除水平线 ``` > ⚠️ **重要：使用关键字参数！** > 所有交易方法（buy/sell/close/set_sl 等）**必须使用关键字参数**传参： > ```python > # ✅ 正确 > client.buy(symbol="XAUUSD.s", lots=0.03) > client.set_sl(ticket=123456, sl=75000) > > # ❌ 错误——按位置传参可能会导致 order 4106（交易被禁用） > client.buy(0.03, "XAUUSD.s") # 顺序可能不对！ > ``` --- ## 网格策略（芝麻网格 EA） | 命令 | 说明 | |------|------| | `client.grid_start(symbol="XAUUSD.s")` | 启动网格 | | `client.grid_stop(symbol="XAUUSD.s")` | 停止网格（不再开新单） | | `client.grid_close_all(symbol="XAUUSD.s")` | 平掉所有网格持仓单 | | `client.grid_status(symbol="XAUUSD.s")` | 查询网格运行状态 | | `client.grid_set_sl_tp(symbol="XAUUSD.s")` | 批量设置网格单的止损止盈 | > ⚠️ **平网格仓必须按以下顺序分两步执行：** > ```python > client.grid_stop(symbol="XAUUSD.s") # 第一步：停止网格 > client.grid_close_all(symbol="XAUUSD.s") # 第二步：平掉所有网格单 > ``` > **不可直接用 `close_all()` 平网格单**。`close_all` 命令会拒绝 magic=777777（网格单）。 > 网格单必须走芝麻网格 EA 自己的关闭通道。 --- ## 价格预警（可选） ```bash cd scripts\ python alert_commands.py profit 20 # 总盈利达到 $20 时提醒 python alert_commands.py above 77000 # BTC 突破 77000 时提醒 python alert_commands.py below 75000 # BTC 跌破 75000 时提醒 python alert_commands.py list # 查看当前所有预警 python alert_commands.py clear # 清除所有预警 python alert_commands.py cancel 3 # 取消第 3 号预警 ``` --- ## 品种代码 | 品种 | 代码 | 说明 | |------|------|------| | 黄金 | `XAUUSD.s` | XAUUSD 后缀 .s 是部分经纪商标准 | | 比特币 | `BTCUSD` | 部分平台为 BTCUSD.pro | | 原油 | `XTIUSD.s` | 美国原油（WTI） | | 白银 | `XAGUSD.s` | 现货白银 | > 品种代码精确格式因经纪商而异。如果 get_price 找不到品种，在 MT4 市场报价中右键 → "全部显示"确认准确的品种名。 --- ## 通信原理 ``` ┌──────────┐ JSON 文件 ┌──────────────┐ │ Python │ ───request────→ │ MT4 EA │ │ Client │ ←──response──── │ (on chart) │ └──────────┘ JSON 文件 └──────────────┘ ``` - **共享目录**：`%APPDATA%\MetaQuotes\Terminal\Common\Files\mt4_bridge\` - 示例：`C:\Users\Administrator\AppData\Roaming\MetaQuotes\Terminal\Common\Files\mt4_bridge\` - **请求文件**：`request_{品种}.json`（如 `request_XAUUSD.s.json`） - **响应文件**：`response_{品种}.json` - **协议**：JSON over 文件（零 DLL 依赖） - **超时**：5 秒（Python 客户端默认配置） - **多品种支持**：多个 EA 实例通过品种专属文件名自动区分 --- ## 常见问题排查 | 现象 | 可能原因 | 解决方法 | |------|---------|---------| | MT4 超时未响应 | EA 未挂载或自动交易未启动 | 确认图表右上角为绿色笑脸 😊 | | error 4106（交易禁用） | EA 属性中"允许实时交易"未勾选 | 右键 EA → 属性 → 勾选"允许实时交易" | | 找不到价格（空结果） | 品种代码不匹配 | 在 MT4 市场报价中确认精确名称 | | tools2.3 编译报错 | 文件放错了目录 | 确认 tools2.3.ex4 在 `MQL4\Libraries\`，**不是** Experts\ | | 重复下单 | 多个 EA 读同一个请求文件 | 使用 v1.84+ 版本，自动使用品种专属文件 | | BTC 无法市价交易 | 部分经纪商禁用 BTC 市价单 | 改用 `buy_limit()`/`sell_limit()` 限价单 | | 重启 MT4 后 EA 不跑 | 自动交易默认关闭 | 手动点绿色播放按钮，或在设置中勾选"启动时启用" | | 芝麻网格周末不运行 | 经纪商周末休市 | 正常现象，周一开市自动恢复 | --- ## 交流与反馈 欢迎加入 **MT4 自动化技术交流群**，一起讨论 MT4 自动交易、Python 量化、EA 开发等话题。 微信搜索：jiaoyibaohe （老码农） 加我好友，备注 **MT4** 即可拉入群聊。 [或进入gitee项目主页，查看群二维码](https://gitee.com/3603317/skill-plugin/tree/master) https://gitee.com/3603317/skill-plugin/tree/master > 📱 扫码添加微信好友，备注 **MT4** 即可拉入技术交流群。 > 群内可交流：安装问题、使用技巧、功能建议、Bug 反馈。 --- ## 版本信息 | 文件 | 版本 | 最后更新 | |------|------|---------| | mt4_bridge.ex4 | v1.86 | 2026-05 | | 芝麻网格V2.ex4 | v1.41 | 2026-03 | | tools2.3.ex4 | v2.3 | 2025-12 | | mt4_client.py | v1.86 | 2026-05-05 | | deploy_ex4.py | v1.0.7 | 2026-05-05 | --- # MT4 Trading Bridge · Automated Trading System MT4 Trading Bridge communicates with MT4 EA via file I/O — zero DLLs, zero extra dependencies. Supports OpenClaw AI Agent to control MT4 trading through WeChat / Feishu / Telegram and other channels. > **Keywords**: MT4, MetaTrader 4, Forex Trading, Python Trading, Algorithmic Trading, Automated Trading, Grid Strategy, Support & Resistance, ATR Stop Loss, Trailing Stop, Gold Trading, Crude Oil Trading, Bitcoin Trading, OpenClaw, AI Trading Assistant --- ## File Structure ``` mt4-trader/ ├── SKILL.md # This documentation ├── mql4/ # MT4 EA compiled files (.ex4, generated by script) │ [Manually download .ex4 files here] ├── scripts/ # Python scripts │ ├── mt4_client.py # Python trading client (core) │ ├── deploy_ex4.py # EA deployment guide (shows download links & steps) │ ├── alert_commands.py # Price alert CLI tool │ ├── position_summary.py # Position summary (CLI output) │ ├── position_summary_cron.py # Position summary (scheduled push) │ ├── sr_strategy.py # Support/Resistance strategy orchestrator │ └── summary_utils.py # Position summary utility module └── references/ └── api_docs.md # Full API reference ``` --- ## Installation ### Step 1: Find Your MT4 Data Directory Each MT4 installation has a unique data directory: ``` %APPDATA%\MetaQuotes\Terminal\<32-char-random-ID> ``` > How to find it? In MT4, press F7 to open MetaEditor, then click File -> Open Data Folder. ### Step 2: Confirm MT4 Directory EA files go into two directories: | EA File | Target Directory | Notes | |---------|-----------------|-------| | mt4_bridge.ex4 | MT4_Install_Dir\MQL4\Experts\ | Core EA (required) | | SesameGridV2.ex4 | MT4_Install_Dir\MQL4\Experts\ | Grid strategy EA (optional) | | tools2.3.ex4 | MT4_Install_Dir\MQL4\Libraries\ | Grid dependency (required if using grid) | > Libraries is at the same level as Experts — NOT a subdirectory of Experts. ### Step 3: Download .ex4 Files From Gitee: https://gitee.com/3603317/skill-plugin/tree/master/mt4 Files: mt4_bridge.ex4 (required), SesameGridV2.ex4 (optional), tools2.3.ex4 (grid dependency). ### Step 4: Attach EAs in MT4 Drag mt4_bridge onto any chart. In properties -> Common tab: check "Allow live trading" and "Allow DLL imports". Green smiley means working. Optional: Attach SesameGridV2 on grid-trading charts. ### Step 5: Enable Auto Trading Click the yellow play icon (unicode triangle) on MT4 toolbar to turn it green. ### Step 6: Verify Run: python mt4_client.py --- ## Core Python Methods ```python from mt4_client import MT4Client client = MT4Client() # Market Data client.get_price("XAUUSD.s") # Real-time quote client.get_positions() # Open positions client.get_account_info() # Account info client.get_history(7) # Order history (7 days) # Trading client.buy(symbol="XAUUSD.s", lots=0.03) client.sell(symbol="XAUUSD.s", lots=0.03) client.close(ticket=123456) client.close_all() # Close all positions # SL/TP client.set_sl(ticket=123456, sl=75000) client.set_tp(ticket=123456, tp=80000) client.modify_order(ticket=123456, sl=75000, tp=80000) client.trailing_stop(ticket=123456, distance=300) # Pending Orders client.buy_limit(symbol="XAUUSD.s", lots=0.03, price=4500.0) client.sell_limit(symbol="XAUUSD.s", lots=0.03, price=4600.0) client.buy_stop(symbol="XAUUSD.s", lots=0.03, price=4700.0) client.sell_stop(symbol="XAUUSD.s", lots=0.03, price=4500.0) client.get_pending_orders() client.cancel_pending(ticket=123456) # Kline Data client.get_klines(symbol="XAUUSD.s", timeframe="H1", count=20) # Support & Resistance client.get_sr_levels(symbol="XAUUSD.s") client.draw_sr_levels(symbol="XAUUSD.s") client.clear_chart_objects(symbol="XAUUSD.s") # Clear all chart objects client.draw_hline(symbol="XAUUSD.s", name="R1", price=4750) # Draw horizontal line client.clear_hline(symbol="XAUUSD.s", prefix="HL_") # Delete lines by prefix ``` > Use keyword arguments for all trading methods! --- ## Grid Strategy | Command | Description | |---------|-------------| | grid_start(symbol) | Start grid | | grid_stop(symbol) | Stop grid | | grid_close_all(symbol) | Close grid orders | | grid_status(symbol) | Check status | For closing grid: call grid_stop() first, then grid_close_all(). --- ## Symbol Codes | Instrument | Code | |------------|------| | Gold | XAUUSD.s | | Bitcoin | BTCUSD | | Crude Oil | XTIUSD.s | | Silver | XAGUSD.s | --- ## Troubleshooting | Issue | Solution | |-------|----------| | MT4 Timeout | EA not attached or auto-trading off | | error 4106 | "Allow live trading" unchecked | | Empty price result | Wrong symbol code | | Duplicate orders | Use v1.84+ for symbol-specific files | | BTC market order fails | Some brokers disable BTC market orders | --- ## Communication & Feedback Join the **MT4 Automated Trading Technical Group** to discuss MT4 auto trading, Python quantitative trading, EA development, and more. Search WeChat: jiaoyibaohe (MT4 Developer) — add me as a friend, note **MT4** to join the group. [Click to view Gitee project homepage, group QR code](https://gitee.com/3603317/skill-plugin/tree/master) > Scan the QR code to add WeChat friend, note **MT4** to join the technical group. > Topics: installation issues, usage tips, feature suggestions, bug reports. ---

path: references/api_docs.md
content: # MT4 Bridge API Reference

## Symbols: XAUUSD.s (Gold), BTCUSD, XAGUSD, XTIUSD.s

## Methods

### Trading
buy(symbol, lots), sell(symbol, lots), close_all(symbol), close_by_magic(symbol, magic)

### Pending Orders
buy_limit(symbol, lots, price), sell_limit(symbol, lots, price), buy_stop(symbol, lots, price), sell_stop(symbol, lots, price), cancel_order(ticket)

### SL/TP
set_sl(symbol, ticket, price), set_tp(symbol, ticket, price), modify_order(symbol, ticket, price, sl, tp)

### Grid (Sesame EA)
grid_start(symbol, direction), grid_stop(symbol), grid_close_all(symbol), grid_status(symbol), grid_set_sl_tp(symbol)

### Chart Objects (v1.86)
draw_sr_levels(symbol, timeframe), clear_chart_objects(symbol), draw_hline(symbol, name, price), clear_hline(symbol, prefix)

### Data
add_alert(symbol, price, tip), get_klines(symbol, timeframe, count), get_positions(symbol), get_notifications(symbol)


path: scripts/alert_commands.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
价格预警管理工具 - 支持子命令的统一 CLI

用法:
  python alert_commands.py add <类型> <品种> <值> [消息]
  python alert_commands.py list
  python alert_commands.py clear

子命令:
  add     添加预警
  list    列出所有预警
  clear   清除所有预警

快捷子命令（默认品种 BTCUSD）:
  python alert_commands.py above <值>           # 突破预警
  python alert_commands.py below <值>           # 跌破预警
  python alert_commands.py profit <值>          # 盈利预警

完整示例:
  python alert_commands.py add above BTCUSD 77000 "BTC 突破 77000"
  python alert_commands.py add profit 20         # 持仓盈利 $20
  python alert_commands.py list
  python alert_commands.py clear
"""
import json
import os
import sys
import logging

logger = logging.getLogger("alert_commands")

_DIR = os.path.dirname(os.path.abspath(__file__))
_ALERT_FILE = os.path.join(_DIR, "alerts.json")


def _load_alerts():
    """加载预警列表。"""
    if os.path.exists(_ALERT_FILE):
        try:
            with open(_ALERT_FILE, "r") as f:
                return json.load(f)
        except (json.JSONDecodeError, OSError) as e:
            logger.error("读取预警文件失败: %s", e)
            return []
    return []


def _save_alerts(alerts):
    """保存预警列表。"""
    try:
        with open(_ALERT_FILE, "w") as f:
            json.dump(alerts, f, ensure_ascii=False, indent=2)
    except OSError as e:
        logger.error("保存预警文件失败: %s", e)
        sys.exit(1)


def cmd_list():
    """列出所有预警。"""
    alerts = _load_alerts()
    if not alerts:
        print("暂无预警。")
        return

    print(f"预警数量: {len(alerts)}")
    for i, a in enumerate(alerts):
        cond = a.get("condition", "?")
        msg = a.get("message", "")
        if a.get("price") is not None:
            detail = f"价格: {a['price']}"
        elif a.get("profit") is not None:
            detail = f"盈利: ${a['profit']}"
        else:
            detail = ""
        print(f"  {i+1}. [{cond}] {msg} ({detail})")


def cmd_clear():
    """清除所有预警。"""
    _save_alerts([])
    print("已清除所有预警。")


def cmd_add(condition, symbol, value, message=None):
    """
    添加预警。

    Args:
        condition: "above" | "below" | "profit"
        symbol: 品种代码
        value: 阈值（float）
        message: 自定义消息（可选）
    """
    try:
        value = float(value)
    except (ValueError, TypeError):
        print(f"❌ 无效的阈值: {value}，请输入数字。")
        sys.exit(1)

    alerts = _load_alerts()
    alert = {"condition": condition, "symbol": symbol.upper()}

    if condition == "profit":
        alert["profit"] = value
        alert["message"] = message or f"持仓盈利超过 ${value}"
    else:
        alert["price"] = value
        alert["message"] = message or f"{symbol} {condition} ${value}"

    alerts.append(alert)
    _save_alerts(alerts)
    print(f"✅ 已添加预警: {alert['message']}")


def _parse_args():
    """
    手动解析参数（保持轻量级，避免额外依赖）。
    不适用 argparse 是因为子命令的参数结构差异较大。
    """
    if len(sys.argv) < 2:
        _print_help()
        sys.exit(0)

    cmd = sys.argv[1].lower()

    if cmd in ("list", "clear"):
        return cmd, None

    if cmd == "add" and len(sys.argv) >= 5:
        # add <condition> <symbol> <value> [message]
        return ("add", {
            "condition": sys.argv[2],
            "symbol": sys.argv[3],
            "value": sys.argv[4],
            "message": sys.argv[5] if len(sys.argv) > 5 else None,
        })

    # 快捷命令: above/below/profit <value>
    if cmd in ("above", "below", "profit") and len(sys.argv) >= 3:
        return ("add", {
            "condition": cmd,
            "symbol": "BTCUSD",  # 快捷命令默认 BTCUSD
            "value": sys.argv[2],
            "message": sys.argv[3] if len(sys.argv) > 3 else None,
        })

    print(f"❌ 未知命令或参数不足: {' '.join(sys.argv[1:])}")
    _print_help()
    sys.exit(1)


def _print_help():
    print(__doc__)


def main():
    cmd, args = _parse_args()

    if cmd == "list":
        cmd_list()
    elif cmd == "clear":
        cmd_clear()
    elif cmd == "add":
        cmd_add(**args)
    else:
        _print_help()


if __name__ == "__main__":
    main()

path: scripts/deploy_ex4.py
content: """
MT4 EA 部署指引脚本
提示用户从 Gitee 手动下载 .ex4 文件并完成部署。

用法:
  python deploy_ex4.py              # 显示下载指引
  python deploy_ex4.py deploy       # 复制已下载的文件到 MT4 目录
"""

import os
import sys
import shutil

GITEE_URL = "https://gitee.com/3603317/skill-plugin/tree/master/mt4"

FILES = [
    ("mt4_bridge.ex4", "Experts", "主程序 EA（必选）"),
    ("\u829d\u9ebb\u7f51\u683cV2.ex4", "Experts", "网格策略 EA（可选）"),
    ("tools2.3.ex4", "Libraries", "网格策略依赖库（选网格则必选）"),
]


def main():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    skill_dir = os.path.dirname(script_dir)
    mql4_dir = os.path.join(skill_dir, "mql4")

    print("=" * 50)
    print("MT4 EA \u90e8\u7f72\u5de5\u5177")
    print("=" * 50)

    print(f"\n[*] \u8bf7\u4ece Gitee \u4ed3\u5e93\u4e0b\u8f7d .ex4 \u6587\u4ef6\uff1a")
    print(f"    {GITEE_URL}\n")

    print(f"    \u9700\u8981\u4e0b\u8f7d\u7684\u6587\u4ef6\uff1a")
    for filename, subdir, desc in FILES:
        raw_url = f"https://gitee.com/3603317/skill-plugin/raw/master/mt4/{filename}"
        print(f"    \u250c {filename}  \u2500 {desc}")
        print(f"    \u2514 {raw_url}")

    if len(sys.argv) <= 1 or sys.argv[1] != "deploy":
        print(f"\n[*] \u4e0b\u8f7d\u5b8c\u6210\u540e\uff0c\u8fd0\u884c\u4ee5\u4e0b\u547d\u4ee4\u90e8\u7f72\uff1a")
        print(f"    python deploy_ex4.py deploy")
        print(f"\n[*] \u6216\u5c06\u6587\u4ef6\u624b\u52a8\u62f7\u8d1d\u5230 MT4 \u5b89\u88c5\u76ee\u5f55")
        print(f"    \u250c MT4\\MQL4\\Experts\\  \u2500 mt4_bridge.ex4, \u829d\u9ebb\u7f51\u683cV2.ex4")
        print(f"    \u2514 MT4\\MQL4\\Libraries\\ \u2500 tools2.3.ex4")
        return

    # deploy mode: copy files from mql4/ to MT4 directories
    os.makedirs(mql4_dir, exist_ok=True)

    experts_dir = None
    libraries_dir = None

    # Auto-detect MT4
    candidates = [
        r"C:\Program Files (x86)\EBC Financial Group Cayman MT4 Terminal\MQL4",
        r"C:\Program Files\MetaTrader 4\MQL4",
        r"C:\Program Files (x86)\MetaTrader 4\MQL4",
        r"C:\Program Files\Alpari MT4\MQL4",
        r"C:\Program Files (x86)\Alpari MT4\MQL4",
    ]
    for c in candidates:
        if os.path.isdir(os.path.join(c, "Experts")):
            experts_dir = os.path.join(c, "Experts")
            libraries_dir = os.path.join(c, "Libraries")
            print(f"\n[*] \u81ea\u52a8\u68c0\u6d4b\u5230 MT4: {c}")
            break

    if not experts_dir:
        print(f"\n[!] \u672a\u81ea\u52a8\u68c0\u6d4b\u5230 MT4 \u5b89\u88c5\u76ee\u5f55")
        return

    missing = []
    for filename, subdir, desc in FILES:
        src = os.path.join(mql4_dir, filename)
        if not os.path.isfile(src):
            missing.append(filename)
            continue
        dst_dir = experts_dir if subdir == "Experts" else libraries_dir
        dst = os.path.join(dst_dir, filename)
        os.makedirs(dst_dir, exist_ok=True)
        shutil.copy2(src, dst)
        print(f"  [+] {filename} \u5df2\u90e8\u7f72 -> {dst}")

    if missing:
        print(f"\n[!] \u4ee5\u4e0b\u6587\u4ef6\u672a\u627e\u5230\uff0c\u8bf7\u5148\u4ece Gitee \u4e0b\u8f7d\uff1a")
        for fn in missing:
            print(f"    - {fn}")

    print(f"\n[+] \u90e8\u7f72\u5b8c\u6210\uff01\u8bf7\u5728 MT4 \u4e2d\u786e\u8ba4:")
    print(f"    - \u5bfc\u822a\u5668\u4e2d\u53ef\u89c1 mt4_bridge.ex4, \u829d\u9ebb\u7f51\u683cV2.ex4")
    print(f"    - MQL4\\\\Libraries\\\\ \u76ee\u5f55\u4e0b\u53ef\u89c1 tools2.3.ex4")


if __name__ == "__main__":
    main()


path: scripts/mt4_client.py
content: MT4 File-Based Bridge Client
Python -> JSON file -> MT4 EA -> Response file -> Python
100% pure file I/O, no DLL imports needed
路径配置（按优先级）:
  1. MT4_BRIDGE_DIR 环境变量
  2. 默认 %APPDATA%\MetaQuotes\Terminal\Common\Files\mt4_bridge
凭证/敏感信息请使用环境变量，不要硬编码在代码中。
import os
import sys
import json
import time
import re
import logging
from ctypes import wintypes
import ctypes
# ---- 日志 ----
logger = logging.getLogger("mt4_client")
# ---- 编码处理 ----
if sys.stdout.encoding != 'utf-8':
    sys.stdout.reconfigure(encoding='utf-8', errors='replace')
# ---- 路径配置 ----
_BRIDGE_DIR = os.environ.get(
    "MT4_BRIDGE_DIR",
    os.path.expandvars(r"%APPDATA%\MetaQuotes\Terminal\Common\Files\mt4_bridge")
)
_GRID_BRIDGE_DIR = os.environ.get(
    "MT4_GRID_BRIDGE_DIR",
    os.path.expandvars(r"%APPDATA%\MetaQuotes\Terminal\Common\Files\grid_bridge")
)
REQUEST_FILE = os.path.join(_BRIDGE_DIR, "request.json")
RESPONSE_FILE = os.path.join(_BRIDGE_DIR, "response.json")
os.makedirs(_BRIDGE_DIR, exist_ok=True)
os.makedirs(_GRID_BRIDGE_DIR, exist_ok=True)
DEFAULT_TIMEOUT = 15
DEFAULT_POLL_INTERVAL = 0.2
class MT4Client:
    MT4 文件桥接客户端。
    Args:
        request_file: 请求文件路径
        response_file: 响应文件路径
        timeout: 默认超时秒数
        poll_interval: 轮询间隔秒数
    def __init__(self, request_file=REQUEST_FILE, response_file=RESPONSE_FILE,
                 timeout=DEFAULT_TIMEOUT, poll_interval=DEFAULT_POLL_INTERVAL):
        self.request_file = request_file
        self.response_file = response_file
        self.timeout = timeout
        self.poll_interval = poll_interval
    def is_ea_running(self):
        try:
            test = self.request_file + ".ping"
            with open(test, "w") as f:
                f.write("ping")
            os.remove(test)
            return True
        except OSError:
            return False
    def wait_for_ea(self, timeout=30):
        logger.info("等待 MT4 EA...（超时: %ds）", timeout)
        start = time.time()
        while time.time() - start < timeout:
            if self.is_ea_running():
                logger.info("MT4 EA 就绪")
                return True
            time.sleep(0.5)
        logger.error("MT4 EA 超时 - 是否已加载 EA？")
        return False
    def _transact(self, request, symbol=None, timeout=None, poll_interval=None):
        发送请求到 EA 并等待响应。
        Args:
            request: 请求字典
            symbol: 交易品种（用于 grid_* 动作的文件路由）
            timeout: 本次超时（秒），None 则使用实例默认值
            poll_interval: 本次轮询间隔（秒），None 则使用实例默认值
        Returns:
            响应字典，超时返回 {"status": "error", "error": "..."}
        timeout = timeout if timeout is not None else self.timeout
        poll_interval = poll_interval if poll_interval is not None else self.poll_interval
        action = request.get("action", "")
        is_grid_action = action.startswith("grid_")
        # ---- 选择文件路径 ----
        if symbol:
            fname = symbol.replace(".", "_").replace("/", "_")
            if is_grid_action:
                req_file = os.path.join(_GRID_BRIDGE_DIR, f"request_{fname}.json")
                resp_file = os.path.join(_GRID_BRIDGE_DIR, f"response_{fname}.json")
            else:
                req_file = os.path.join(_BRIDGE_DIR, f"request_{fname}.json")
                resp_file = os.path.join(_BRIDGE_DIR, f"response_{fname}.json")
        elif is_grid_action:
            return {"status": "error", "error": "grid action requires symbol parameter"}
        else:
            req_file = self.request_file
            resp_file = self.response_file
        # ---- 清理旧响应文件 ----
        _safe_remove(resp_file)
        # ---- 写入请求 ----
        try:
            with open(req_file, "w", encoding="utf-8") as f:
                json.dump(request, f, ensure_ascii=False, separators=(',', ':'))
            logger.debug("写入请求 -> %s", os.path.basename(req_file))
        except Exception as e:
            return {"status": "error", "error": f"写入请求失败: {e}"}
        # ---- 轮询等待响应 ----
        start = time.time()
        while True:
            elapsed = time.time() - start
            if elapsed >= timeout:
                logger.warning("请求超时（%ds）: %s", timeout, action)
                return {"status": "error", "error": f"MT4 超时 ({timeout}s)"}
            if os.path.exists(resp_file):
                try:
                    time.sleep(0.15)
                    response = _read_json_file(resp_file)
                    _safe_remove(resp_file)
                    logger.debug("收到响应（%.2fs）: %s", elapsed, action)
                    return response
                except Exception as e:
                    _safe_remove(resp_file)
                    return {"status": "error", "error": f"读取响应失败: {e}"}
            time.sleep(poll_interval)
            if int(elapsed) % 2 == 0 and elapsed > 1:
                logger.debug("等待中... %.1fs", elapsed)
    def get_price(self, symbol):
        return self._transact({"action": "get_price", "params": {"symbol": symbol}})
    def get_account_info(self):
        return self._transact({"action": "get_account_info", "params": {}})
    def get_auto_trading(self):
        return self._transact({"action": "get_auto_trading", "params": {}})
    def get_positions(self):
        return self._transact({"action": "get_positions", "params": {}})
    def buy(self, symbol, lots=0.01, price=0):
        return self._transact({
            "action": "buy",
            "params": {"symbol": symbol, "lots": str(lots), "price": str(price)}
        })
    def sell(self, symbol, lots=0.01, price=0):
        return self._transact({
            "action": "sell",
            "params": {"symbol": symbol, "lots": str(lots), "price": str(price)}
        })
    def close(self, ticket):
        return self._transact({"action": "close", "params": {"ticket": str(ticket)}})
    def close_all_buy(self):
        return self._transact({"action": "close_all_buy", "params": {}})
    def close_all_sell(self):
        return self._transact({"action": "close_all_sell", "params": {}})
    def close_all(self):
        return self._transact({"action": "close_all", "params": {}})
    def close_profit(self):
        return self._transact({"action": "close_profit", "params": {}})
    def close_loss(self):
        return self._transact({"action": "close_loss", "params": {}})
    def close_by_magic(self, magic, symbol=None):
        params = {"magic": str(magic)}
        if symbol:
            params["symbol"] = symbol
        return self._transact({"action": "close_by_magic", "params": params})
    def safe_close_all(self, symbol):
        安全平掉所有订单：先停网格再平仓，防止边平边开。
        流程：
        1. grid_close_all — 停止网格 + 平掉网格订单
        2. close_all — 平掉剩余所有订单
        即使网格 EA 没运行，第 1 步超时后第 2 步仍会执行。
        result = {"steps": []}
        r1 = self.grid_close_all(symbol=symbol, timeout=8)
        result["steps"].append({"step": "grid_close_all", "result": r1})
        grid_success = r1.get("success") is True
        if grid_success:
            time.sleep(1)
        r2 = self.close_all()
        result["steps"].append({"step": "close_all", "result": r2})
        result["grid_was_running"] = grid_success
        result["status"] = "ok"
        return result
    def get_pending_orders(self):
        return self._transact({"action": "get_pending", "params": {}})
    def cancel_pending(self, ticket):
        return self._transact({"action": "cancel_pending", "params": {"ticket": str(ticket)}})
    def buy_limit(self, symbol, lots, price, sl=0, tp=0):
        return self._transact({
            "action": "buy_limit",
            "params": {"symbol": symbol, "lots": str(lots), "price": str(price),
                       "sl": str(sl), "tp": str(tp)}
        })
    def sell_limit(self, symbol, lots, price, sl=0, tp=0):
        return self._transact({
            "action": "sell_limit",
            "params": {"symbol": symbol, "lots": str(lots), "price": str(price),
                       "sl": str(sl), "tp": str(tp)}
        })
    def buy_stop(self, symbol, lots, price, sl=0, tp=0):
        return self._transact({
            "action": "buy_stop",
            "params": {"symbol": symbol, "lots": str(lots), "price": str(price),
                       "sl": str(sl), "tp": str(tp)}
        })
    def sell_stop(self, symbol, lots, price, sl=0, tp=0):
        return self._transact({
            "action": "sell_stop",
            "params": {"symbol": symbol, "lots": str(lots), "price": str(price),
                       "sl": str(sl), "tp": str(tp)}
        })
    def modify_order(self, ticket, sl=None, tp=None):
        修改现有订单的止损和/或止盈。
        None = 保持当前值，0 = 移除。
        Args:
            ticket: 订单号
            sl: 新止损价（None=保持，0=移除）
            tp: 新止盈价（None=保持，0=移除）
        Example:
            modify_order(123456, sl=78431.86, tp=76431.86)
            modify_order(123456, sl=78000)
            modify_order(123456, tp=80000)
            modify_order(123456, sl=0)
        params = {"ticket": str(ticket)}
        if sl is not None:
            params["sl"] = str(sl)
        if tp is not None:
            params["tp"] = str(tp)
        return self._transact({"action": "modify_order", "params": params})
    def set_sl(self, ticket, sl_price):
        return self._transact({
            "action": "set_sl",
            "params": {"ticket": str(ticket), "sl": str(sl_price)}
        })
    def set_sl_all(self, symbol, sl_price):
        return self._transact({
            "action": "set_sl_all",
            "params": {"symbol": symbol, "sl": str(sl_price)}
        })
    def set_tp(self, ticket, tp_price):
        return self._transact({
            "action": "set_tp",
            "params": {"ticket": str(ticket), "tp": str(tp_price)}
        })
    def set_tp_by_profit(self, profit, sl_profit=None, symbol=None):
        params = {"profit": str(profit)}
        if sl_profit is not None:
            params["sl_profit"] = str(sl_profit)
        if symbol:
            params["symbol"] = symbol
        return self._transact({"action": "set_tp_by_profit", "params": params})
    def set_sl_tp_all(self, symbol, sl=0, tp=0):
        return self._transact({
            "action": "set_sl_tp_all",
            "params": {"symbol": symbol, "sl": str(sl), "tp": str(tp)}
        })
    def partial_close(self, ticket, lots):
        return self._transact({
            "action": "partial_close",
            "params": {"ticket": str(ticket), "lots": str(lots)}
        })
    def auto_sl(self, ticket, risk_percent=2.0):
        return self._transact({
            "action": "auto_sl",
            "params": {"ticket": str(ticket), "risk_percent": str(risk_percent)}
        })
    def trailing_stop(self, ticket, trail_distance=500):
        return self._transact({
            "action": "trailing_stop",
            "params": {"ticket": str(ticket), "trail_distance": str(trail_distance)}
        })
    def atr_sl(self, ticket, atr_period=14, atr_multiplier=2.0):
        return self._transact({
            "action": "atr_sl",
            "params": {"ticket": str(ticket), "atr_period": str(atr_period),
                       "atr_multiplier": str(atr_multiplier)}
        })
    def oco_order(self, symbol, lots, order1, order2):
        params = {
            "symbol": symbol,
            "lots": str(lots),
            "order1": {"type": order1["type"], "price": str(order1["price"])},
            "order2": {"type": order2["type"], "price": str(order2["price"])}
        }
        for key in ("sl", "tp"):
            if key in order1 and order1[key] > 0:
                params["order1"][key] = str(order1[key])
            if key in order2 and order2[key] > 0:
                params["order2"][key] = str(order2[key])
        return self._transact({"action": "oco", "params": params})
    def grid_close_all(self, symbol, timeout=None):
        return self._transact({"action": "grid_close_all"}, symbol=symbol, timeout=timeout)
    def grid_close_all_buy(self, symbol):
        return self._transact({"action": "grid_close_buy"}, symbol=symbol)
    def grid_close_all_sell(self, symbol):
        return self._transact({"action": "grid_close_sell"}, symbol=symbol)
    def grid_stop(self, symbol):
        return self._transact({"action": "grid_stop"}, symbol=symbol)
    def grid_start(self, symbol):
        return self._transact({"action": "grid_start"}, symbol=symbol)
    def grid_start_buy(self, symbol):
        return self._transact({"action": "grid_start_buy"}, symbol=symbol)
    def grid_start_sell(self, symbol):
        return self._transact({"action": "grid_start_sell"}, symbol=symbol)
    def grid_status(self, symbol):
        return self._transact({"action": "grid_status"}, symbol=symbol)
    def grid_set_sl_tp(self, symbol, tp_profit_usd=None, sl_points=None):
        params = {"symbol": symbol}
        if tp_profit_usd is not None:
            params["tp"] = str(tp_profit_usd)
        if sl_points is not None:
            params["sl"] = str(sl_points)
        return self._transact({"action": "grid_set_sl_tp", "params": params}, symbol=symbol)
    def get_history(self, days=7):
        return self._transact({
            "action": "get_history",
            "params": {"days": str(days)}
        })
    def calc_lots(self, symbol, risk_percent=1.0, sl_distance=0):
        return self._transact({
            "action": "calc_lots",
            "params": {"symbol": symbol, "risk_percent": str(risk_percent),
                       "sl_distance": str(sl_distance)}
        })
    def get_klines(self, symbol, timeframe="H1", count=10):
        return self._transact({
            "action": "get_klines",
            "params": {"symbol": symbol, "timeframe": timeframe, "count": str(count)}
        })
    def set_auto_trading(self, enabled=True):
        if enabled == "toggle":
            val = "toggle"
        else:
            val = "true" if enabled else "false"
        return self._transact({"action": "set_auto_trading", "params": {"enabled": val}})
    def toggle_auto_trading_global(self, window_title=None, desired_state=None):
        通过模拟 Ctrl+E 切换 MT4 全局自动交易。
        使用 SendInput API（替代弃用的 keybd_event）+ Alt 键技巧
        绕过前台锁限制，即使 MT4 不是活动窗口也能工作。
        Args:
            window_title: MT4 窗口标题（可选，自动检测）
            desired_state: "ON" 或 "OFF" — 先检查状态，需要时才切换
        Returns:
            {"status": "ok", "message": "..."} 或 {"status": "error", "error": "..."}
        from ctypes import wintypes
        import ctypes
        _u32 = ctypes.windll.user32
        # ---- 自动检测 MT4 窗口 ----
        if not window_title:
            window_title = self._find_mt4_window_title(_u32)
            if not window_title:
                return {"status": "error", "error": "未找到 MT4 终端窗口"}
        try:
            hwnd = _u32.FindWindowW(None, window_title)
            if not hwnd:
                return {"status": "error", "error": f"未找到窗口: {window_title}"}
            # ---- 如指定了目标状态，先检查当前状态 ----
            if desired_state and desired_state.upper() in ("ON", "OFF"):
                check = self._transact(
                    {"action": "get_terminal_auto_trading", "params": {}}
                )
                current = check.get("terminal_auto_trading", "").upper()
                if current == desired_state.upper():
                    return {"status": "ok", "message": f"已是 {desired_state.upper()}，无需切换"}
            # ---- 发送 Ctrl+E ----
            self._send_ctrl_e(_u32, hwnd)
            return {"status": "ok", "message": "Ctrl+E 已发送到 MT4 终端"}
        except Exception as e:
            logger.exception("切换自动交易失败")
            return {"status": "error", "error": str(e)}
    def _find_mt4_window_title(self, _u32):
        WNDENUMPROC = ctypes.WINFUNCTYPE(
            ctypes.c_bool, wintypes.HWND, wintypes.LPARAM
        )
        _found = []
        def _enum_cb(hwnd, _lparam):
            length = _u32.GetWindowTextLengthW(hwnd)
            if length > 0:
                buf = ctypes.create_unicode_buffer(length + 1)
                _u32.GetWindowTextW(hwnd, buf, length + 1)
                t = buf.value
                if "EBCFinancial" in t and "MetaEditor" not in t:
                    _found.append(t)
            return True
        _u32.EnumWindows(WNDENUMPROC(_enum_cb), 0)
        return _found[0] if _found else None
    def _send_ctrl_e(self, _u32, hwnd):
        SPI_SETFOREGROUNDLOCKTIMEOUT = 0x2001
        SPIF_SENDCHANGE = 0x0002
        _u32.SystemParametersInfoW(
            SPI_SETFOREGROUNDLOCKTIMEOUT, 0, 0, SPIF_SENDCHANGE
        )
        VK_MENU = 0x12
        KEYEVENTF_KEYUP = 0x0002
        _u32.keybd_event(VK_MENU, 0, 0, 0)
        _u32.keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0)
        time.sleep(0.1)
        _u32.ShowWindow(hwnd, 9)
        _u32.SetForegroundWindow(hwnd)
        _u32.BringWindowToTop(hwnd)
        time.sleep(1.0)
        INPUT_KEYBOARD = 1
        KEYEVENTF_SCANCODE = 0x0008
        class KEYBDINPUT(ctypes.Structure):
            _fields_ = [
                ("wVk", wintypes.WORD),
                ("wScan", wintypes.WORD),
                ("dwFlags", ctypes.c_ulong),
                ("time", ctypes.c_ulong),
                ("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong)),
            ]
        class INPUT_u(ctypes.Union):
            _fields_ = [("ki", KEYBDINPUT)]
        class INPUT(ctypes.Structure):
            _fields_ = [
                ("type", ctypes.c_ulong),
                ("u", INPUT_u),
            ]
        ctrl_down = INPUT()
        ctrl_down.type = INPUT_KEYBOARD
        ctrl_down.u.ki.wVk = 0x11
        ctrl_down.u.ki.dwFlags = 0
        e_down = INPUT()
        e_down.type = INPUT_KEYBOARD
        e_down.u.ki.wVk = 0x45
        e_down.u.ki.dwFlags = 0
        e_up = INPUT()
        e_up.type = INPUT_KEYBOARD
        e_up.u.ki.wVk = 0x45
        e_up.u.ki.dwFlags = KEYEVENTF_KEYUP
        ctrl_up = INPUT()
        ctrl_up.type = INPUT_KEYBOARD
        ctrl_up.u.ki.wVk = 0x11
        ctrl_up.u.ki.dwFlags = KEYEVENTF_KEYUP
        inputs = (INPUT * 4)(ctrl_down, e_down, e_up, ctrl_up)
        sent = ctypes.windll.user32.SendInput(4, ctypes.byref(inputs),
                                              ctypes.sizeof(INPUT))
        if sent != 4:
            logger.warning("SendInput 只发送了 %d/4 个输入", sent)
    def get_sr_levels(self, symbol, timeframe=60, strength=5, max_levels=5):
        tf = self._resolve_timeframe(timeframe)
        return self._transact({
            "action": "get_sr_levels",
            "params": {"symbol": symbol, "timeframe": tf,
                       "strength": strength, "max_levels": max_levels}
        })
    def draw_sr_levels(self, symbol, timeframe=60, strength=5, max_levels=5):
        tf = self._resolve_timeframe(timeframe)
        return self._transact({
            "action": "draw_sr_levels",
            "params": {"symbol": symbol, "timeframe": tf,
                       "strength": strength, "max_levels": max_levels}
        })
    @staticmethod
    def _resolve_timeframe(timeframe):
        tf_map = {
            1: 1, 5: 5, 15: 15, 30: 30,
            60: 60, 240: 240, 1440: 1440, 10080: 10080, 43200: 43200
        }
        return tf_map.get(timeframe, 60)
    def add_alert(self, symbol, condition="above", target=0, message=""):
        if not message:
            message = f"{symbol} {condition} {target}"
        return self._transact({
            "action": "add_alert",
            "params": {"symbol": symbol, "condition": condition,
                       "target": str(target), "message": message}
        })
    def list_alerts(self):
        return self._transact({"action": "list_alerts", "params": {}})
    def cancel_alert(self, slot):
        return self._transact({
            "action": "cancel_alert",
            "params": {"slot": str(slot)}
        })
    def get_notifications(self):
        return self._transact({"action": "get_notifications", "params": {}})
    def clear_chart_objects(self, symbol):
        return self._transact({
            "action": "clear_chart_objects",
            "params": {"symbol": symbol}
        })
    def draw_hline(self, symbol, name, price, color='Yellow', width=1, style='solid', label=''):
        在图表上画自定义水平线。
        参数:
            symbol: 品种名
            name: 线的名称（会自动加前缀 "HL_"）
            price: 价格位置
            color: 颜色 (Red/Green/Blue/Orange/Yellow/Cyan/Magenta/Lime/White/Gray)
            width: 线宽 (1-5)
            style: 样式 (solid/dash/dot/dashdot)
            label: 标签文字（显示在图表上）
        return self._transact({
            "action": "draw_hline",
            "params": {
                "symbol": symbol,
                "name": name,
                "price": float(price),
                "color": color,
                "width": int(width),
                "style": style,
                "label": label
            }
        })
    def clear_hline(self, symbol, prefix='HL_'):
        按前缀删除水平线。
        参数:
            symbol: 品种名
            prefix: 前缀（默认 "HL_"，删除所有 HL_ 开头的线）
        return self._transact({
            "action": "clear_hline",
            "params": {"symbol": symbol, "prefix": prefix}
        })
def _safe_remove(path):
    try:
        if os.path.exists(path):
            os.remove(path)
    except OSError:
        pass
def _read_json_file(filepath):
    读取 JSON 文件，自动尝试多种编码。
    EA 的 FileWriteString 输出可能为 GBK/ANSI。
    raw = None
    for enc in ['utf-8', 'gbk', 'latin-1']:
        try:
            with open(filepath, "r", encoding=enc) as f:
                raw = f.read()
            break
        except (UnicodeDecodeError, UnicodeError):
            continue
    if raw is None:
        raise ValueError("无法解码响应文件（尝试了 utf-8/gbk/latin-1）")
    raw = _fix_ea_json_quoting(raw)
    return json.loads(raw)
def _fix_ea_json_quoting(raw):
    修复 EA MQL4 生成的 JSON 中的双层引号问题。
    EA 端可能生成: {""status"": ""ok""} 而不是 {"status": "ok"}
    使用正则精确修复，避免破坏字符串值中的合法双引号。
    raw = re.sub(r'""(\w+)"":', r'"\1":', raw)
    raw = re.sub(r':""([^"]*?)""', r':"\1"', raw)
    raw = re.sub(r'\[""', r'["', raw)
    raw = re.sub(r'""\]', r'"]', raw)
    raw = re.sub(r',""', r',"', raw)
    raw = re.sub(r'"",', r'",', raw)
    return raw
def main():
    logger.info("=" * 50)
    logger.info("MT4 File Bridge - Python Client 测试")
    logger.info("=" * 50)
    logger.info("桥接目录: %s", _BRIDGE_DIR)
    logger.info("请求文件: %s", REQUEST_FILE)
    logger.info("响应文件: %s", RESPONSE_FILE)
    print()
    client = MT4Client()
    if not client.wait_for_ea():
        logger.error("提示: 请先在 MT4 图表上加载 mt4_bridge EA")
        return
    logger.info("[TEST] get_price(XAUUSD)...")
    r = client.get_price(symbol="XAUUSD.s")
    print(json.dumps(r, indent=2, ensure_ascii=False))
    logger.info("[TEST] get_positions()...")
    r = client.get_positions()
    print(json.dumps(r, indent=2, ensure_ascii=False))
    logger.info("[DONE]")
if __name__ == "__main__":
    logging.basicConfig(
        level=logging.DEBUG,
        format="%(asctime)s [%(levelname)s] %(name)s %(message)s",
        datefmt="%H:%M:%S"
    )
    main()


path: scripts/position_summary.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
持仓汇总 - 命令行输出版本

用于定时任务执行（如 cron），无持仓时自动退出。

依赖: mt4_client.py, summary_utils.py
"""
import sys
import os
import logging

# 动态路径，无需硬编码
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mt4_client import MT4Client
from summary_utils import build_position_summary

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger("position_summary")


def main():
    logger.info("开始获取持仓数据...")
    c = MT4Client()

    try:
        r = c.get_positions()
    except Exception as e:
        logger.exception("获取持仓失败")
        sys.exit(1)

    summary, count, total_pl = build_position_summary(r)

    if count == 0:
        print(summary)
        logger.info("当前空仓，任务结束。")
        # 不自动取消 cron 任务 —— 调度器应自行决策
        return

    print(summary)
    logger.info("持仓汇总已输出（共 %d 单，总盈亏 $%.2f）", count, total_pl)


if __name__ == "__main__":
    main()

path: scripts/position_summary_cron.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
持仓汇总 - 消息推送版本

通过 Webhook 将持仓汇总推送到消息通道（微信等）。
使用环境变量配置凭证，避免硬编码。

环境变量:
  SUMMARY_WEBHOOK_URL   - Webhook 接口地址 (默认 http://localhost:1608/api/message/send)
  SUMMARY_USER_ID       - 接收消息的用户 ID
  SUMMARY_ACCOUNT_ID    - 发送账号 ID
  SUMMARY_CHANNEL       - 消息通道 (默认 openclaw-weixin)

依赖: mt4_client.py, summary_utils.py
"""
import sys
import os
import logging

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mt4_client import MT4Client
from summary_utils import build_position_summary

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger("position_summary_cron")

# ---- 配置 ----
WEBHOOK_URL = os.environ.get(
    "SUMMARY_WEBHOOK_URL",
    "http://localhost:1608/api/message/send"
)
USER_ID = os.environ.get("SUMMARY_USER_ID", "")
ACCOUNT_ID = os.environ.get("SUMMARY_ACCOUNT_ID", "")
CHANNEL = os.environ.get("SUMMARY_CHANNEL", "openclaw-weixin")


def send_message(msg):
    """通过 Webhook 发送消息。"""
    if not USER_ID or not ACCOUNT_ID:
        logger.warning("SUMMARY_USER_ID 或 SUMMARY_ACCOUNT_ID 未设置，跳过消息发送。")
        logger.info("消息内容:\n%s", msg)
        return True

    try:
        import requests
    except ImportError:
        logger.error("缺少 requests 库，无法发送消息。请执行: pip install requests")
        return False

    payload = {
        "action": "send",
        "channel": CHANNEL,
        "to": USER_ID,
        "message": msg,
        "accountId": ACCOUNT_ID
    }
    try:
        resp = requests.post(WEBHOOK_URL, json=payload, timeout=10)
        if resp.status_code == 200:
            logger.info("消息发送成功")
            return True
        else:
            logger.error("消息发送失败，HTTP %s: %s", resp.status_code, resp.text[:200])
            return False
    except requests.exceptions.Timeout:
        logger.error("消息发送超时（%s）", WEBHOOK_URL)
        return False
    except requests.exceptions.ConnectionError:
        logger.error("无法连接到 Webhook 服务: %s", WEBHOOK_URL)
        return False
    except Exception as e:
        logger.exception("消息发送异常")
        return False


def main():
    logger.info("开始获取持仓数据...")
    c = MT4Client()

    try:
        r = c.get_positions()
    except Exception as e:
        logger.exception("获取持仓失败")
        send_message(f"❌ 获取持仓失败: {e}")
        return

    summary, count, total_pl = build_position_summary(r)

    if count == 0:
        msg = "✅ 当前已空仓，停止发送订单汇总。"
        logger.info(msg)
    else:
        msg = summary

    send_message(msg)


if __name__ == "__main__":
    main()

path: scripts/sr_strategy.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""支撑压力位交易策略 - Python 编排脚本

功能:
1. 启用策略时与用户确认参数
2. 查看当前策略状态和持仓信息
3. 停止策略

用法:
  python sr_strategy.py --enable                    # 启用策略(交互式确认参数)
  python sr_strategy.py --status                    # 查看策略状态
  python sr_strategy.py --stop                      # 停止策略

注意: 本脚本不执行实际交易逻辑。策略由 MT4 端的 sr_strategy.mq4 EA 执行。
      Python 脚本仅负责参数管理和状态查询。
"""
import sys
import os
import json
import logging
from datetime import datetime

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mt4_client import MT4Client

logger = logging.getLogger("sr_strategy")

# ---- 默认参数 ----
DEFAULT_CONFIG = {
    "max_loss_percent": 1.0,   # 最大亏损 %
    "entry_timeframe": "M15",  # 入场周期
    "breakout_thresh": 0.003,  # 突破阈值 0.3%
    "stop_thresh": 0.005,      # 止损阈值 0.5%
    "symbols": ["XAUUSD.s"],   # 交易品种
    "max_positions": 2,        # 每品种最大持仓
    "enabled": False           # 是否启用
}

CONFIG_FILE = os.path.join(os.path.dirname(__file__), "sr_strategy_config.json")


def load_config():
    """加载配置，文件不存在则返回默认配置。"""
    if os.path.exists(CONFIG_FILE):
        try:
            with open(CONFIG_FILE, "r", encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, OSError) as e:
            logger.error("读取配置失败: %s，使用默认配置", e)
    return DEFAULT_CONFIG.copy()


def save_config(config):
    """保存配置。"""
    try:
        with open(CONFIG_FILE, "w", encoding="utf-8") as f:
            json.dump(config, f, indent=2, ensure_ascii=False)
        logger.info("配置已保存到 %s", CONFIG_FILE)
    except OSError as e:
        logger.error("保存配置失败: %s", e)
        sys.exit(1)


def enable_strategy():
    """启用策略 — 交互式确认参数。"""
    config = load_config()

    print("\n" + "=" * 50)
    print("支撑压力位交易策略 - 参数确认")
    print("=" * 50)
    print(f"\n当前配置:")
    print(f"  1. 最大亏损%: {config['max_loss_percent']}%")
    print(f"  2. 入场周期: {config['entry_timeframe']}")
    print(f"  3. 突破阈值: {config['breakout_thresh'] * 100}%")
    print(f"  4. 止损阈值: {config['stop_thresh'] * 100}%")
    print(f"  5. 交易品种: {config['symbols']}")
    print(f"  6. 每品种最大持仓: {config['max_positions']}")

    print("\n按回车使用默认值，或输入新值:")

    try:
        val = input(f"1. 最大亏损% [{config['max_loss_percent']}]: ").strip()
        if val:
            config['max_loss_percent'] = float(val)

        val = input(f"2. 入场周期 [{config['entry_timeframe']}]: ").strip()
        if val:
            config['entry_timeframe'] = val.upper()

        val = input(f"3. 突破阈值% [{config['breakout_thresh'] * 100}]: ").strip()
        if val:
            config['breakout_thresh'] = float(val) / 100

        val = input(f"4. 止损阈值% [{config['stop_thresh'] * 100}]: ").strip()
        if val:
            config['stop_thresh'] = float(val) / 100

        val = input(f"5. 交易品种 [{','.join(config['symbols'])}]: ").strip()
        if val:
            config['symbols'] = [s.strip() for s in val.split(',')]

        val = input(f"6. 每品种最大持仓 [{config['max_positions']}]: ").strip()
        if val:
            config['max_positions'] = int(val)

    except KeyboardInterrupt:
        print("\n已取消")
        return
    except ValueError as e:
        print(f"❌ 输入格式错误: {e}")
        return

    config['enabled'] = True
    save_config(config)

    print("\n" + "=" * 50)
    print("✅ 策略已启用!")
    print("=" * 50)
    print(f"配置已保存到: {CONFIG_FILE}")
    print("\n策略将在 MT4 中自动运行。")
    print("请确保 sr_strategy.mq4 EA 已挂载到图表上。")


def show_status():
    """显示策略状态和当前持仓。"""
    config = load_config()

    print("\n" + "=" * 50)
    print("支撑压力位交易策略 - 状态")
    print("=" * 50)
    print(f"\n策略状态: {'✅ 已启用' if config.get('enabled') else '❌ 未启用'}")

    if not config.get('enabled'):
        return

    print(f"\n配置参数:")
    print(f"  最大亏损%: {config['max_loss_percent']}%")
    print(f"  入场周期: {config['entry_timeframe']}")
    print(f"  突破阈值: {config['breakout_thresh'] * 100}%")
    print(f"  止损阈值: {config['stop_thresh'] * 100}%")
    print(f"  交易品种: {config['symbols']}")
    print(f"  每品种最大持仓: {config['max_positions']}")

    c = MT4Client()

    # 查询当前持仓
    print(f"\n当前持仓:")
    try:
        r = c.get_positions()
        if r.get('count', 0) == 0:
            print("  无持仓")
        else:
            for p in r.get('positions', []):
                if p.get('symbol') in config.get('symbols', []):
                    print(f"  {p['symbol']} {p['type']} {p['lots']}手 @ {p['price']} "
                          f"P/L: ${p.get('profit', 0):.2f}")
    except Exception as e:
        print(f"  ⚠️ 获取持仓失败: {e}")

    # 查询 SR 位
    print(f"\n支撑压力位:")
    for sym in config.get('symbols', []):
        try:
            sr = c.get_sr_levels(symbol=sym)
            if sr.get('status') == 'ok':
                print(f"  {sym}:")
                print(f"    阻力: {sr.get('resistance', [])}")
                print(f"    支撑: {sr.get('support', [])}")
            else:
                print(f"  {sym}: 获取失败 ({sr.get('error', '未知错误')})")
        except Exception as e:
            print(f"  {sym}: 获取失败 - {e}")


def stop_strategy():
    """停止策略。"""
    config = load_config()
    config['enabled'] = False
    save_config(config)
    print("✅ 策略已停止")


def main():
    import argparse

    parser = argparse.ArgumentParser(description="支撑压力位交易策略")
    parser.add_argument("--enable", action="store_true", help="启用策略")
    parser.add_argument("--status", action="store_true", help="查看状态")
    parser.add_argument("--stop", action="store_true", help="停止策略")

    args = parser.parse_args()

    if args.enable:
        enable_strategy()
    elif args.status:
        show_status()
    elif args.stop:
        stop_strategy()
    else:
        parser.print_help()


if __name__ == "__main__":
    main()

path: scripts/summary_utils.py
content: # -*- coding: utf-8 -*-
"""
持仓汇总核心逻辑 - 共享模块
供 position_summary.py 和 position_summary_cron.py 共用
"""
import logging

logger = logging.getLogger(__name__)


def build_position_summary(raw_positions_response):
    """
    从 MT4 持仓响应中构建汇总文本。

    Args:
        raw_positions_response: MT4Client.get_positions() 的返回结果

    Returns:
        (summary_text, count, total_pl) 三元组。
        异常时返回 (错误信息, 0, 0.0)
    """
    try:
        count = raw_positions_response.get('count', 0)
        positions = raw_positions_response.get('positions', [])

        if count == 0 or not positions:
            return "✅ 当前无持仓。", 0, 0.0

        lines = [f"📊 **订单汇总**（共{count}单）\n"]

        symbols = {}
        total_pl = 0.0

        for p in positions:
            sym = p.get('symbol', '?')
            ptype = p.get('type', 'buy')
            lots = p.get('lots', 0)
            profit = p.get('profit', 0.0)

            if sym not in symbols:
                symbols[sym] = {
                    'buy': {'lots': 0, 'count': 0, 'pl': 0},
                    'sell': {'lots': 0, 'count': 0, 'pl': 0}
                }
            symbols[sym][ptype]['lots'] += lots
            symbols[sym][ptype]['count'] += 1
            symbols[sym][ptype]['pl'] += profit
            total_pl += profit

        for sym, data in symbols.items():
            buy_lots = data['buy']['lots']
            sell_lots = data['sell']['lots']
            net = buy_lots - sell_lots
            if net > 0:
                net_str = f"净多{net:.2f}"
            elif net < 0:
                net_str = f"净空{abs(net):.2f}"
            else:
                net_str = "持平"
            lines.append(
                f"**{sym}**: 多{buy_lots:.2f}手({data['buy']['count']}单) / "
                f"空{sell_lots:.2f}手({data['sell']['count']}单) → {net_str}"
            )

        lines.append(f"\n**总盈亏**: ${total_pl:.2f}")
        return '\n'.join(lines), count, total_pl

    except Exception as e:
        logger.exception("构建持仓汇总时出错")
        return f"❌ 构建汇总失败: {e}", 0, 0.0

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)