From 6f3389bc198da24e22fdf86d67da677079db35a1 Mon Sep 17 00:00:00 2001 From: gavindiaz Date: Thu, 9 Jul 2026 05:58:55 +0000 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=98=93=E7=A2=B0=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API使用指南.md | 1142 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1142 insertions(+) create mode 100644 API使用指南.md diff --git a/API使用指南.md b/API使用指南.md new file mode 100644 index 0000000..d3840cd --- /dev/null +++ b/API使用指南.md @@ -0,0 +1,1142 @@ +# Mt5Bridge API 使用指南 + +> 本文档面向**开发者**,假设 Bridge 已在云端部署运行。直接复制代码即可使用。 + +--- + +## 连接信息 + +| 项目 | 值 | +|------|-----| +| 地址 | `http://61.164.252.86:13485` | +| 认证 | `X-API-Key` Header 或 `?key=` URL 参数 | +| 格式 | 所有返回均为 JSON | +| WebSocket | `ws://61.164.252.86:13485`,握手时用 Header `X-API-Key` | +| SSE | `http://.../stream/ticks-sse/{symbol}`,Header 或 `?key=` 均可 | + +> **浏览器注意**:原生 `WebSocket` 不支持自定义 Header,须用 `?key=` URL 参数;SSE 用 `new EventSource(url + '?key=...')` 同理。Node/Python/Java 客户端用 Header 更干净。 + +--- + +## 快速开始(Python) + +```python +import requests + +BRIDGE = "http://61.164.252.86:13485" +KEY = "your-api-key" + +def api(path, params=None): + """统一请求封装""" + resp = requests.get(f"{BRIDGE}{path}", params=params, headers={"X-API-Key": KEY}) + resp.raise_for_status() + return resp.json() + +def api_post(path, data): + """POST 请求封装""" + resp = requests.post(f"{BRIDGE}{path}", json=data, headers={"X-API-Key": KEY}) + resp.raise_for_status() + return resp.json() + +# 测试连接 +print(api("/health")) +``` + +--- + +## API 接口速查 + +| 分类 | 方法 | 端点 | 用途 | +|------|------|------|------| +| **系统** | `GET` | `/health` | 健康检查 + MT5 连接状态 | +| **账户** | `GET` | `/account` | 余额/净值/保证金/杠杆 | +| **行情** | `GET` | `/symbols/{symbol}` | 品种信息(点值/手数/合约) | +| | `GET` | `/symbols/{symbol}/tick` | 拉取一次 tick | +| | `WS` | `/stream/ticks/{symbol}` | **实时 tick 推送**(推荐) | +| | `GET` | `/stream/ticks-sse/{symbol}` | SSE 推送(浏览器/内网代理友好) | +| **K 线** | `GET` | `/rates/from-pos` | K 线(按偏移量) | +| | `GET` | `/rates/from-date` | **K 线(按时间范围)⭐** | +| **持仓** | `GET` | `/positions[?symbol=]` | 当前持仓列表 | +| | `POST` | `/position/close` | 平仓(全/部分) | +| | `POST` | `/position/modify` | 改 SL/TP | +| | `POST` | `/position/close-by` | 对冲平仓(节省点差) | +| | `POST` | `/positions/close-batch` | 批量平仓(按 magic/symbol) | +| **挂单** | `GET` | `/orders[?symbol=]` | 挂单列表 | +| | `POST` | `/order/cancel` | 撤单 | +| | `POST` | `/order/modify` | 改挂单价/SL/TP | +| **下单** | `POST` | `/order/check` | 预检(不成交) | +| | `POST` | `/order/send` | 实际下单 | +| **历史** | `GET` | `/history/deals` | 历史成交 | +| **信号** | `GET/POST/DELETE` | `/gvar[/{name}]` | MQL5 全局变量(指标信号桥) | + +> 推送类端点(WS/SSE):每品种最多 50 个订阅者,超出返回 `429`;每 30 秒发一条心跳包用于穿透 NAT 保持连接。 + +--- + +## ⚠️ 隐含约定与已知陷阱(先读) + +下面这些坑都是踩过的,**不读这节直接调接口几乎必踩**: + +### P1. `/history/deals` 的 `date_to` 是 **EXCLUSIVE**(不含当天) + +```bash +# ❌ 0 deals — 07-08 当天全部丢失 +GET /history/deals?date_from=2026-07-06&date_to=2026-07-08 + +# ✅ 42 deals — date_to 设成"明天"才能取到 07-08 当天 +GET /history/deals?date_from=2026-07-08&date_to=2026-07-09 +``` + +**规则**:永远把 `date_to` 设为"目标日期的下一天"。 + +### P2. `/history/deals` 的 `entry` 字段语义跟 MT5 标准 **相反** + +``` +bridge entry = 1 ⇒ OUT(关仓)—— profit 字段是已实现 P&L(USD) +bridge entry = 0 ⇒ IN (开仓)—— profit 固定为 0 +``` + +MT5 MQL5 原生约定是 `DEAL_ENTRY_IN=0 / DEAL_ENTRY_OUT=1`,这个 bridge 的 C# 实现把语义反过来了。 +**如果不验证就用 close 路径过滤 deal,会一个都匹配不到**(结果 P&L 永远是 0)。 + +```python +# ✅ 正确:找关仓 deal +exits = [d for d in deals if d.get('entry') == 1 and d.get('magic') == 88001] +``` + +### P3. `/order/send` 只返回 `{retcode, order, comment}`,**没有成交价、没有 deal ticket** + +```json +// 实际响应(成功) +{"data": {"retcode": 10009, "order": 1797395084, "comment": "Request executed"}} +``` + +bridge 不返回 `price` 也不返回 `deal` 字段。**所以拿真实 fill 价格和实现 P&L,唯一办法是 close 之后查 `/history/deals`**。 + +```python +# ❌ 永远拿不到正确价格 +result = api_post('/order/send', {...})['data'] +result.get('price') # None + +# ✅ 正确:成交后从 history/deals 拿 +deals = api('/history/deals', params={'date_from': today, 'date_to': tomorrow})['data'] +exit_deal = next(d for d in deals if d['entry'] == 1 and d['symbol'] == sym) +realized_pnl_usd = exit_deal['profit'] # broker 已经换算成 deposit currency +actual_fill_price = exit_deal['price'] +``` + +### P4. `/positions.profit` **是 deposit currency(USD),不是 quote currency** + +```python +# USDCAD SELL 当前浮动 -0.17 +# 这是 USD 真实值 (-0.24 CAD ÷ 1.417 USD/CAD = -0.169 USD ≈ -0.17) +# 不是 CAD! +``` + +MT5 `POSITION_PROFIT` 的官方语义就是 in deposit currency,bridge 严格遵循。 +**如果手动算 USDCAD / USDJPY P&L 时按 quote currency 处理,会差一个汇率倍数**(USDCAD 大约 1.4×)。 + +### P5. bridge 拿到的 tick 跟 broker 实际 fill 差 1-4 ticks + +`/positions` 的 `price_open` / `price_current` 和 `/order/send` 时看到的 tick 跟 broker 服务器**真实成交价**有几毫秒级的时间差,导致值差 0.0001-0.0004(约 0.1-0.4 pip)。 + +``` +broker 实际 fill: 1.41715 +/positions.price_open: 1.41711 ← 差 0.00004 (0.4 pip) +``` + +**永远以 `/history/deals` 里的 `price` 为准做对账,不要用 `/positions` 的 price_open**。 + +### P6. `/position/close` 与 `/order/send` 的成功 retcode 含义不同 + +| 端点 | 成功 retcode | 失败 retcode | 来源 | +|------|--------------|--------------|------| +| `/order/send` | MT5 原生(通常 `10009`) | MT5 原生 | 直接透传 `result.Retcode` | +| `/position/close` | **合成** `10009` | **合成** `10004` | C# 代码 `ok ? 10009u : 10004u` | + +两者都是 `10009 = success`,但**别假设 0 是 success**。建议统一判 `retcode == 10009`。 + +### P7. `data: []` 与"默认值数据"的语义混淆 + +Bridge 的所有 list 端点(`/account`, `/positions`, `/orders`, `/history/deals`, `/symbols` 等)都遵循这个统一约定: + +- **"有数据"** ⇒ `{"data": [{...}], "count": 1, ...}` +- **"无数据"** ⇒ `{"data": [], "count": 0, ...}`(**HTTP 仍 200**,不是错误) + +客户端很容易把"无数据"误判成"默认值数据"。例: + +```python +# ❌ 错:data:[] 时 fallback 到 {},默认零值看着像合法数据 +info = api('/account') +acc = (info.get('data') or [{}])[0] +if not acc.get('trade_allowed'): + raise PermissionError('trading disabled') + # ↑ 实际可能是"账户未登录",不是"Algo Trading 真关闭" + +# ❌ 错:bool({})==True,让健康检查通过 +if api('/account'): + mark_healthy() +``` + +**正确做法**:用 `data` 长度 + 身份字段(如 `login`、`ticket`)作为"真实数据就绪"的标志: + +```python +def data_ready(info, key='login'): + """data 非空 + 身份字段 > 0 ⇒ 真实数据""" + items = info.get('data') or [] + return bool(items) and (items[0].get(key, 0) if items else 0) not in (0, None, '') + +# ✅ +info = api('/account') +if not data_ready(info, 'login'): + raise ConnectionError('account not loaded yet') +``` + +**适用所有 list 端点**。判 `positions`、`orders`、`deals` 同理。 + +--- + +## 1. 健康检查 + +``` +GET /health +``` + +```python +status = api("/health") +# {"status": "healthy", "mt5_connected": true, "api_version": "1.0.0"} +``` + +--- + +## 2. 账户信息 + +``` +GET /account +``` + +```python +acc = api("/account")["data"][0] +print(f"余额: {acc['balance']}, 净值: {acc['equity']}, 浮动盈亏: {acc['profit']}") +print(f"保证金: {acc['margin']}, 可用保证金: {acc['margin_free']}, 比例: {acc['margin_level']}%") +print(f"杠杆: 1:{acc['leverage']}, 币种: {acc['currency']}") +``` + +**返回字段:** + +| 字段 | 含义 | +|------|------| +| login | 账户号 | +| balance | 余额 | +| equity | 净值 | +| profit | 浮动盈亏 | +| margin | 已用保证金 | +| margin_free | 可用保证金 | +| margin_level | 保证金比例 | +| leverage | 杠杆 | +| currency | 账户币种 | +| trade_allowed | 是否允许交易 | +| trade_expert | 是否允许 EA 交易 | + +--- + +## 3. 实时行情 + +#### 3-1. 主动拉取(一次性) + +``` +GET /symbols/{symbol}/tick +``` + +自动将品种加入 MT5 Market Watch,并等待最多 3 秒获取真实 tick 数据(解决品种未订阅时返回空值的问题)。 + +```python +def get_tick(symbol): + data = api(f"/symbols/{symbol}/tick")["data"][0] + return data["bid"], data["ask"] + +bid, ask = get_tick("XAUUSD") +print(f"XAUUSD Bid: {bid} Ask: {ask} Spread: {ask - bid}") +``` + +**返回字段:** + +| 字段 | 含义 | +|------|------| +| bid | 卖价 | +| ask | 买价 | +| last | 最新成交价 | +| volume | 成交量 | +| time | 时间 | + +#### 3-2. 订阅推送(WebSocket 流式)⭐ 推荐 + +``` +WS /stream/ticks/{symbol} +``` + +MT5 每收到一个 tick 就立即推给所有订阅者,省去轮询。 + +- 走 `X-API-Key` 认证(Header `X-API-Key: your-key`,WebSocket 客户端在握手 Header 里带) +- 连上时自动把品种加入 MT5 Market Watch;最后一个订阅者断开时自动移除 +- 每个品种最多 50 个订阅者(含 WS + SSE 总数),超出返回 `429` +- 每 30 秒发一条心跳(无 tick 时也发),客户端可用于保活与断线检测 + +**推送格式(每条 tick 一帧 JSON 文本):** + +```json +{ + "type": "tick", + "symbol": "XAUUSDc", + "time": "2026-07-08T10:30:45", + "bid": 4180.0, + "ask": 4180.5, + "last": 4180.2, + "volume": 100, + "time_msc": "2026-07-08T10:30:45.123000", + "flags": 6 +} +``` + +**心跳包:** + +```json +{"type":"heartbeat","time":"2026-07-08T10:31:15"} +``` + +#### 3-3. SSE 推送(不能用 WebSocket 的环境) + +``` +GET /stream/ticks-sse/{symbol} → Content-Type: text/event-stream +``` + +面向无法建 WebSocket 的客户端(部分老浏览器、内网代理、curl 测试等)。语义同 3-2,每条 tick 一帧 SSE: + +``` +data: {"type":"tick","symbol":"XAUUSDc","bid":4180.0,...} + +data: {"type":"tick","symbol":"XAUUSDc","bid":4181.0,...} + +``` + +**curl 测试:** + +```bash +curl -N -H "X-API-Key: your-api-key" \ + http://61.164.252.86:13485/stream/ticks-sse/XAUUSDc +``` + +**Python SSE 客户端(`sseclient-py`):** + +```python +from sseclient import SSEClient +import json + +messages = SSEClient("http://61.164.252.86:13485/stream/ticks-sse/XAUUSDc", + headers={"X-API-Key": "your-api-key"}) +for msg in messages: + data = json.loads(msg.data) + if data.get("type") == "heartbeat": + continue + print(data["symbol"], data["bid"], data["ask"]) +``` + +**Python 示例(需安装 `websocket-client`):** + +```python +import websocket +import threading + +def on_message(ws, msg): + tick = eval(msg) # 或 json.loads(msg) + print(f"{tick['symbol']} Bid:{tick['bid']} Ask:{tick['ask']}") + +def on_open(ws): + print("connected") + +def on_close(ws, code, reason): + print(f"disconnected: {code} {reason}") + +ws = websocket.WebSocketApp( + f"ws://61.164.252.86:13485/stream/ticks/XAUUSDc", + header=[f"X-API-Key: your-api-key"], + on_message=on_message, + on_open=on_open, + on_close=on_close, +) +ws.run_forever() +``` + +**`websockets` 库(asyncio 版):** + +```python +import asyncio +import websockets +import json + +async def watch_ticks(): + headers = {"X-API-Key": "your-api-key"} + async with websockets.connect( + "ws://61.164.252.86:13485/stream/ticks/XAUUSDc", + additional_headers=headers, + ) as ws: + async for raw in ws: + tick = json.loads(raw) + print(tick["symbol"], tick["bid"], tick["ask"]) + +asyncio.run(watch_ticks()) +``` + +**浏览器控制台测试:** + +```js +const ws = new WebSocket("ws://61.164.252.86:13485/stream/ticks/XAUUSDc", { + headers: { "X-API-Key": "your-api-key" } // 浏览器原生 WS 不支持自定义 Header,需走 ?key= 参数,见下 +}); +// 浏览器场景:用 query 参数传 key +const ws2 = new WebSocket("ws://61.164.252.86:13485/stream/ticks/XAUUSDc?key=your-api-key"); +ws2.onmessage = (e) => console.log(JSON.parse(e.data)); +``` + +--- + +## 4. 品种信息 + +``` +GET /symbols/{symbol} +``` + +bid/ask 从实时 tick 数据获取(自动等待最多 3 秒),避免品种刚加入 Market Watch 时返回 0 的问题。 + +```python +def get_symbol_info(symbol): + info = api(f"/symbols/{symbol}")["data"][0] + print(f"品种: {info['name']}, 描述: {info['description']}") + print(f"小数位: {info['digits']}, 点值: {info['point']}") + print(f"最小手数: {info['volume_min']}, 最大: {info['volume_max']}, 步长: {info['volume_step']}") + print(f"合约大小: {info['trade_contract_size']}") + return info +``` + +--- + +## 5. 历史 K 线(按偏移量) + +``` +GET /rates/from-pos?symbol={symbol}&timeframe={timeframe}&start_pos={start}&count={count} +``` + +| 参数 | 可选值 | +|------|--------| +| timeframe | `TIMEFRAME_M1` / `M5` / `M15` / `M30` / `H1` / `H4` / `D1` | +| start_pos | 0 = 最新,1 = 前一根,以此类推 | +| count | 获取数量,最大 10000 | + +```python +import pandas as pd + +def get_rates(symbol, timeframe, count): + """获取 K 线并转为 DataFrame""" + data = api("/rates/from-pos", params={ + "symbol": symbol, + "timeframe": f"TIMEFRAME_{timeframe}", + "start_pos": 0, + "count": count + })["data"] + df = pd.DataFrame(data) + df["time"] = pd.to_datetime(df["time"]) + df.set_index("time", inplace=True) + return df + +# 获取最近 100 根 H1 K 线 +df = get_rates("XAUUSD", "H1", 100) +print(df.head()) +``` + +**返回字段:** `time`, `open`, `high`, `low`, `close`, `tick_volume`, `spread`, `real_volume` + +--- + +### 5-2. 历史 K 线(按时间范围)⭐ 推荐 + +``` +GET /rates/from-date?symbol={symbol}&timeframe={timeframe}&date_from={date_from}&date_to={date_to} +``` + +| 参数 | 可选值 | +|------|--------| +| timeframe | `TIMEFRAME_M1` / `M5` / `M15` / `M30` / `H1` / `H4` / `D1` | +| date_from | 起始日期,ISO-8601 或 `yyyy-MM-dd` | +| date_to | 结束日期,ISO-8601 或 `yyyy-MM-dd` | + +```python +def get_rates_by_date(symbol, timeframe, date_from, date_to): + """按时间范围获取 K 线""" + data = api("/rates/from-date", params={ + "symbol": symbol, + "timeframe": f"TIMEFRAME_{timeframe}", + "date_from": date_from, + "date_to": date_to, + })["data"] + df = pd.DataFrame(data) + df["time"] = pd.to_datetime(df["time"]) + df.set_index("time", inplace=True) + return df + +# 获取 2026年7月1日 ~ 7月3日 的 H1 K 线 +df = get_rates_by_date("XAUUSD", "H1", "2026-07-01", "2026-07-03") +print(df.head()) +``` + +**返回字段:** 同 `/rates/from-pos` + +--- + +## 6. 当前持仓 + +``` +GET /positions[?symbol={symbol}] +``` + +symbol 可选过滤。 + +```python +def get_positions(symbol=None): + return api("/positions", params={"symbol": symbol} if symbol else None)["data"] + +positions = get_positions() +for pos in positions: + print(f"{pos['ticket']} {pos['symbol']} " + f"{'买' if pos['type'] == 0 else '卖'} " + f"手数:{pos['volume']} 盈亏:{pos['profit']}") +``` + +**返回字段:** `ticket`, `symbol`, `type`(0=买,1=卖), `volume`, `price_open`, `sl`, `tp`, `price_current`, `swap`, `profit`, `comment`, `magic` + +### 6-1. 平仓 + +``` +POST /position/close +``` + +```json +// 全平 +{ "ticket": 12345678 } +// 部分平仓 +{ "ticket": 12345678, "volume": 0.05 } +``` + +| 字段 | 必填 | 说明 | +|------|------|------| +| ticket | ✅ | 持仓编号 | +| volume | ❌ | 平仓手数;不传/0 = 全平;>0 = 部分平仓 | +| deviation | ❌ | 允许滑点(默认 10) | + +```python +def close_position(ticket, volume=None): + body = {"ticket": ticket} + if volume: body["volume"] = volume + return api_post("/position/close", body)["data"] + +close_position(12345678) # 全平 +close_position(12345678, volume=0.05) # 部分平 +``` + +### 6-2. 改持仓 SL/TP + +``` +POST /position/modify +``` + +```json +{ "ticket": 12345678, "sl": 4170.0, "tp": 4190.0 } +``` + +SL/TP 设为 0 表示清除对应止损/止盈。 + +```python +def modify_position(ticket, sl=0, tp=0): + return api_post("/position/modify", {"ticket": ticket, "sl": sl, "tp": tp})["data"] +``` + +### 6-3. 对冲平仓(节省点差) + +``` +POST /position/close-by +``` + +用一张反向持仓对冲平仓,只收一次点差(MT5 净额结算),适合双向网格 / 锁仓策略快速离场。 + +```json +{ "position": 111, "position_by": 222 } +``` + +要求:两张持仓 **同品种 + 反向**。 + +```python +def close_by(ticket_a, ticket_b): + return api_post("/position/close-by", {"position": ticket_a, "position_by": ticket_b})["data"] +``` + +### 6-4. 移动止损(客户端轮询模式) + +Bridge 不在服务端跑轮询,暴露 `/position/modify` 由客户端自己做: + +```python +def trail_stop(ticket, distance, step): + """distance: 跟踪距离(如 50 点);step: 最小推进步长(如 10 点)""" + pos = next((p for p in bridge.positions() if p["ticket"] == ticket), None) + if not pos: + return + current = pos["price_current"] + if pos["type"] == 0: # 多单 + new_sl = current - distance + if new_sl - pos["sl"] >= step: + bridge.modify_position(ticket, sl=new_sl) + else: # 空单 + new_sl = current + distance + if pos["sl"] - new_sl >= step: + bridge.modify_position(ticket, sl=new_sl) + +# 每秒跑一次 +while True: + for p in bridge.positions(): + trail_stop(p["ticket"], distance=50, step=10) + time.sleep(1) +``` + +也可以用 WebSocket tick 流推送驱动,把 `time.sleep(1)` 换成 tick 回调,反应更快。 + +### 6-5. 批量平仓 + +``` +POST /positions/close-batch +``` + +按 `symbol` 和/或 `magic` 批量平仓。**至少传一个**过滤条件,避免误清整个账户。 + +```json +{ "magic": 123456 } +{ "symbol": "XAUUSDc", "magic": 123456 } +{ "symbol": "XAUUSDc" } +``` + +| 字段 | 必填 | 说明 | +|------|------|------| +| symbol | 一 | 品种过滤 | +| magic | 一 | Magic Number 过滤 | +| deviation | ❌ | 允许滑点(默认 10) | + +```python +def close_by_magic(magic): + return api_post("/positions/close-batch", {"magic": magic})["data"] + +result = close_by_magic(123456) +print(f"已平 {result['closed']} 单,失败 {result['failed']} 单") +# data 数组里有每张单的 ticket / symbol / retcode / comment +``` + +--- + +## 7. 挂单 + +``` +GET /orders?symbol={symbol} +``` + +symbol 可选,不传返回全部。 + +```python +orders = api("/orders")["data"] +for o in orders: + print(f"{o['ticket']} {o['symbol']} 类型:{o['type']} 手数:{o['volume_initial']}") +``` + +### 7-1. 撤单 + +``` +POST /order/cancel +``` + +```json +{ "ticket": 87654321 } +``` + +```python +def cancel_order(ticket): + return api_post("/order/cancel", {"ticket": ticket})["data"] + +cancel_order(87654321) +``` + +### 7-2. 改挂单 + +``` +POST /order/modify +``` + +```json +{ "ticket": 87654321, "price": 4180.0, "sl": 4170.0, "tp": 4190.0 } +``` + +sl/tp 设为 0 表示清除。 + +```python +def modify_order(ticket, price, sl=0, tp=0): + return api_post("/order/modify", {"ticket": ticket, "price": price, "sl": sl, "tp": tp})["data"] +``` + +--- + +## 8. 订单预检 + +``` +POST /order/check +``` + +下单前验证,不会真正执行。检查保证金是否足够、价格是否有效等。 + +**填充模式自动适配**:服务端会根据品种的 `SYMBOL_FILLING_MODE` 自动选择经纪商支持的填充模式。如果请求的 `type_filling` 不被支持,会按 IOC(1) → FOK(0) → RETURN(2) 顺序降级,无需客户端手动判断。 + +```python +def check_order(symbol, volume, order_type, price, sl=None, tp=None, magic=0, comment=""): + """预检订单""" + data = { + "action": 1, # 1=即时成交 + "symbol": symbol, + "volume": volume, + "order_type": order_type, # 0=市价买, 1=市价卖 + "price": price, + "sl": sl or 0, + "tp": tp or 0, + "magic": magic, + "comment": comment, + "deviation": 10, + "type_filling": 0 # 0=FOK, 1=IOC, 2=RETURN — 服务端自动适配,不传也行 + } + result = api_post("/order/check", data)["data"] + print(f"预检结果: retcode={result['retcode']}, comment={result['comment']}") + if result['retcode'] == 0: + print("✅ 可以下单") + else: + print("❌ 不可下单") + return result + +check_order("XAUUSD", 0.01, 0, 4180.0, sl=4170.0, tp=4190.0) +``` + +**type_filling 说明:** + +| 值 | 含义 | 说明 | +|------|------|------| +| 0 | FOK (Fill or Kill) | 必须全部成交,否则取消 | +| 1 | IOC (Immediate or Cancel) | 能成交多少成交多少 | +| 2 | RETURN | 剩余部分留在订单簿 | + +> 不同经纪商支持的填充模式不同(如 ICMarkets 只支持 IOC),服务端会自动降级,客户端无需关心。 + +--- + +## 9. 下单 + +``` +POST /order/send +``` + +与 `/order/check` 相同,`type_filling` 会自动适配经纪商支持的填充模式。 + +```python +def send_order(symbol, volume, order_type, price, sl=None, tp=None, magic=0, comment=""): + """下单""" + data = { + "request": { + "action": 1, + "symbol": symbol, + "volume": volume, + "order_type": order_type, + "price": price, + "sl": sl or 0, + "tp": tp or 0, + "magic": magic, + "comment": comment, + "deviation": 10, + "type_filling": 0 # 自动适配,不传也行 + } + } + result = api_post("/order/send", data)["data"] + print(f"下单结果: retcode={result['retcode']}, order={result['order']}, comment={result['comment']}") + return result + +# 市价买入 0.01 手 XAUUSD +result = send_order("XAUUSD", 0.01, 0, 4180.0, sl=4170.0, tp=4190.0) +``` + +**order_type 说明:** + +| 值 | 含义 | +|------|------| +| 0 | 市价买入 | +| 1 | 市价卖出 | +| 2 | 限价买入 | +| 3 | 限价卖出 | +| 4 | 止损买入 | +| 5 | 止损卖出 | + +--- + +## 10. 历史成交 + +``` +GET /history/deals?date_from={from}&date_to={to}&symbol={symbol} +``` + +```python +deals = api("/history/deals", params={ + "date_from": "2026-07-01", + "date_to": "2026-07-03", + "symbol": "XAUUSD" +})["data"] + +for d in deals: + print(f"{d['ticket']} {d['time']} {d['symbol']} " + f"手数:{d['volume']} 价格:{d['price']} 盈亏:{d['profit']}") +``` + +--- + +## 11. 全局变量(MQL5 信号桥) + +> 让 MQL5 指标/EA 把信号写出来,Python 通过 Bridge 读取。不用翻译 MQL5 代码。 + +说明: + +- 这部分属于“MT5 指标/EA 信号导出”能力,不影响 Bridge 的账户、行情、历史、持仓、挂单、预检、下单等基础 API。 +- 如果你只更新了远程 `Mt5Bridge.dll`,没有替换 `Alpha Trend.ex5`,Bridge 仍然可以正常工作,`/gvar` 也仍会返回旧版指标写出的变量名。 +- 只有在你需要“已收盘 K 线信号”和“带周期/参数作用域的新变量名”时,才需要重新编译并替换新版 `Alpha Trend.ex5`。 + +#### 列出所有全局变量 + +``` +GET /gvar +``` + +```python +gvars = api("/gvar") +print(gvars) +# {"data": [{"name": "AT_Trend_XAUUSD", "value": 1}, ...], "count": 5} +``` + +#### 读取指定变量 + +``` +GET /gvar/{name} +``` + +```python +trend = api("/gvar/AT_Trend_XAUUSD")["value"] +print(f"趋势方向: {'多头' if trend == 1 else '空头'}") +``` + +#### 写入变量 + +``` +POST /gvar/{name} +``` + +Body: `{"value": 75.5}` + +```python +def set_gvar(name, value): + return api_post(f"/gvar/{name}", {"value": value}) + +set_gvar("MY_RSI", 75.5) +``` + +#### 删除变量 + +``` +DELETE /gvar/{name} +``` + +--- + +## MQL5 指标 → Python 完整流程 + +### 第一步:改指标源码,输出已收盘 K 线信号 + +这一步是“升级指标导出行为”的可选步骤,不是 Bridge 基础 API 的必需步骤。 + +```mql5 +// 在 OnCalculate 末尾加 +int last_closed = rates_total - 2; +string key = StringFormat("MY_SIGNAL_%s_%s", _Symbol, EnumToString(_Period)); +GlobalVariableSet(key, signal_value[last_closed]); +``` + +### 第二步:Python 读取信号 + +```python +def read_signal(): + try: + return api(f"/gvar/MY_SIGNAL_XAUUSD_PERIOD_H1")["value"] + except: + return None + +signal = read_signal() +print(f"指标信号: {signal}") +``` + +如果你仍在使用旧版 `Alpha Trend.ex5`,则读取方式应继续对应旧键名,例如: + +```python +trend = api("/gvar/AT_Trend_XAUUSD")["value"] +buy_signal = api("/gvar/AT_Buy_XAUUSD")["value"] +``` + +### 第三步:根据信号做决策 + +```python +def on_tick(): + signal = read_signal() + if signal != 1: + return # 没信号,不动 + + if not bridge.has_position("XAUUSD"): + bid, ask = bridge.tick("XAUUSD") + bridge.buy("XAUUSD", 0.01, ask, sl=ask - 50, tp=ask + 100) + print("指标发出买入信号,已开多") +``` + +## 完整策略模板 + +```python +import requests +import pandas as pd +import time +from datetime import datetime + +BRIDGE = "http://61.164.252.86:13485" +KEY = "your-api-key" + +class Mt5Bridge: + def __init__(self): + self.headers = {"X-API-Key": KEY} + + def _get(self, path, params=None): + r = requests.get(f"{BRIDGE}{path}", params=params, headers=self.headers) + r.raise_for_status() + return r.json() + + def _post(self, path, data): + r = requests.post(f"{BRIDGE}{path}", json=data, headers=self.headers) + r.raise_for_status() + return r.json() + + def stream_ticks(self, symbol, on_tick): + """订阅 tick 推送,on_tick 回调收到 dict,阻塞运行。需 pip install websocket-client""" + import websocket + url = f"{BRIDGE.replace('http', 'ws', 1)}/stream/ticks/{symbol}" + ws = websocket.WebSocketApp( + url, + header=[f"X-API-Key: {KEY}"], + on_message=lambda ws, msg: on_tick(eval(msg)), + on_error=lambda ws, err: print(f"ws error: {err}"), + ) + ws.run_forever() + + def stream_ticks_async(self, symbol, on_tick): + """后台线程订阅 tick,不阻塞主线程""" + import threading + t = threading.Thread(target=self.stream_ticks, args=(symbol, on_tick), daemon=True) + t.start() + return t + + # ── 行情 ── + def tick(self, symbol): + d = self._get(f"/symbols/{symbol}/tick")["data"][0] + return d["bid"], d["ask"] + + def rates(self, symbol, timeframe, count): + return self._get("/rates/from-pos", params={ + "symbol": symbol, "timeframe": f"TIMEFRAME_{timeframe}", + "start_pos": 0, "count": count + })["data"] + + def to_df(self, symbol, timeframe, count): + df = pd.DataFrame(self.rates(symbol, timeframe, count)) + df["time"] = pd.to_datetime(df["time"]) + df.set_index("time", inplace=True) + return df + + # ── 账户 ── + def account(self): + return self._get("/account")["data"][0] + + # ── 持仓 ── + def positions(self, symbol=None): + return self._get("/positions", params={"symbol": symbol} if symbol else None)["data"] + + def has_position(self, symbol): + return any(p["symbol"] == symbol for p in self.positions()) + + def close(self, ticket, volume=None): + body = {"ticket": ticket} + if volume: body["volume"] = volume + return self._post("/position/close", body)["data"] + + def modify_position(self, ticket, sl=0, tp=0): + return self._post("/position/modify", {"ticket": ticket, "sl": sl, "tp": tp})["data"] + + def close_by(self, ticket_a, ticket_b): + return self._post("/position/close-by", {"position": ticket_a, "position_by": ticket_b})["data"] + + def close_batch(self, magic=None, symbol=None, deviation=None): + body = {k: v for k, v in {"magic": magic, "symbol": symbol, "deviation": deviation}.items() if v is not None} + return self._post("/positions/close-batch", body) + + # ── 挂单 ── + def orders(self, symbol=None): + return self._get("/orders", params={"symbol": symbol} if symbol else None)["data"] + + def cancel_order(self, ticket): + return self._post("/order/cancel", {"ticket": ticket})["data"] + + def modify_order(self, ticket, price, sl=0, tp=0): + return self._post("/order/modify", {"ticket": ticket, "price": price, "sl": sl, "tp": tp})["data"] + + # ── 下单 ── + def buy(self, symbol, volume, price, sl=0, tp=0, magic=0, comment=""): + return self._send(symbol, volume, 0, price, sl, tp, magic, comment) + + def sell(self, symbol, volume, price, sl=0, tp=0, magic=0, comment=""): + return self._send(symbol, volume, 1, price, sl, tp, magic, comment) + + def _send(self, symbol, volume, order_type, price, sl, tp, magic, comment): + return self._post("/order/send", { + "request": { + "action": 1, "symbol": symbol, "volume": volume, + "order_type": order_type, "price": price, + "sl": sl, "tp": tp, "magic": magic, + "comment": comment, "deviation": 10 + } + })["data"] + + def check(self, symbol, volume, order_type, price, sl=0, tp=0): + return self._post("/order/check", { + "action": 1, "symbol": symbol, "volume": volume, + "order_type": order_type, "price": price, + "sl": sl, "tp": tp, "magic": 0, "comment": "", "deviation": 10 + })["data"] + + +# ══════════════════════════════════════════════ +# 策略示例:均线金叉死叉 +# ══════════════════════════════════════════════ + +class MAStrategy: + def __init__(self, bridge, symbol, fast=20, slow=60): + self.bridge = bridge + self.symbol = symbol + self.fast = fast + self.slow = slow + + def signal(self): + """计算信号:1=买入, -1=卖出, 0=观望""" + df = self.bridge.to_df(self.symbol, "H1", self.slow + 5) + df["ma_fast"] = df["close"].rolling(self.fast).mean() + df["ma_slow"] = df["close"].rolling(self.slow).mean() + + # 使用最近两根已收盘 K 线 + prev = df.iloc[-3] + curr = df.iloc[-2] + + # 金叉 + if prev["ma_fast"] <= prev["ma_slow"] and curr["ma_fast"] > curr["ma_slow"]: + return 1 + # 死叉 + if prev["ma_fast"] >= prev["ma_slow"] and curr["ma_fast"] < curr["ma_slow"]: + return -1 + return 0 + + def run(self): + sig = self.signal() + bid, ask = self.bridge.tick(self.symbol) + acc = self.bridge.account() + print(f"[{datetime.now()}] {self.symbol} Bid:{bid} Ask:{ask} " + f"Balance:{acc['balance']} Equity:{acc['equity']} Signal:{sig}") + + if sig == 1 and not self.bridge.has_position(self.symbol): + print(" → 金叉,开多") + self.bridge.buy(self.symbol, 0.01, ask, sl=ask - 50, tp=ask + 100) + elif sig == -1 and not self.bridge.has_position(self.symbol): + print(" → 死叉,开空") + self.bridge.sell(self.symbol, 0.01, bid, sl=bid + 50, tp=bid - 100) + + +# ══════════════════════════════════════════════ +# 运行 +# ══════════════════════════════════════════════ + +if __name__ == "__main__": + bridge = Mt5Bridge() + strategy = MAStrategy(bridge, "XAUUSD", fast=20, slow=60) + + while True: + try: + strategy.run() + except Exception as e: + print(f"Error: {e}") + time.sleep(60) # 每分钟检查一次 +``` + +--- + +## 浏览器快速验证 + +在浏览器地址栏直接输入: + +``` +http://61.164.252.86:13485/health?key=your-api-key +http://61.164.252.86:13485/account?key=your-api-key +http://61.164.252.86:13485/symbols/XAUUSD/tick?key=your-api-key +http://61.164.252.86:13485/rates/from-date?symbol=XAUUSD&timeframe=TIMEFRAME_H1&date_from=2026-07-01&date_to=2026-07-03&key=your-api-key +``` + +--- + +## PowerShell 快速测试 + +```powershell +$headers = @{ "X-API-Key" = "your-api-key" } +Invoke-RestMethod "http://61.164.252.86:13485/health" -Headers $headers +Invoke-RestMethod "http://61.164.252.86:13485/account" -Headers $headers +Invoke-RestMethod "http://61.164.252.86:13485/symbols/XAUUSD/tick" -Headers $headers +Invoke-RestMethod "http://61.164.252.86:13485/rates/from-date?symbol=XAUUSD&timeframe=TIMEFRAME_H1&date_from=2026-07-01&date_to=2026-07-03" -Headers $headers +``` + +--- + +## 常见问题 + +> **第一次调这个 bridge?先去读 [§「⚠️ 隐含约定与已知陷阱」](#-隐含约定与已知陷阱先读)** —— 7 个非显而易见的约定(date_to 排他、`entry` 字段语义反、`/order/send` 不返回 fill 价、`/account` 空 data 误判 等),不读这一节直接调接口几乎必踩。 + +### 返回 "Unauthorized" +API Key 错误或没带。检查 Header 中的 `X-API-Key` 或 URL 中的 `?key=`。 + +### 返回 "对于该符号,不支持市场执行" +`order_type` 填错了,MT5 中有些品种不支持市价单,有些不支持挂单。先调用 `/order/check` 预检。 + +### 返回 "没有足够的资金" +保证金不足,减小手数或检查 `account.margin_free`。 + +### 闭仓 P&L 永远是 0 / 跟 MT5 terminal 对不上 +大概率是过滤了 `entry==0` 的 deal(以为 OUT),实际这个 bridge 是 `entry==1` 才是 OUT(关仓)。详见 §隐含约定 P2。 + +### 算出来的 P&L 跟 broker 对差 1.4× / 1.5× +多半是手动把 `positions.profit` 当 quote currency 处理。`positions.profit` 是 deposit currency(USD),不是 quote currency。详见 §隐含约定 P4。 + +### 返回 "无法连接到远程服务器" +Bridge 未运行或网络不通,先检查 `/health`。 + +### 业务字段返回默认值 → 怎么区分"无数据"和"真状态" +`/account` 在账户未加载完成时返回 `data:[]` + 全零字段(HTTP 200)。客户端若只看 `bool(response)` 或 `field == 0`,会把"无数据"误判成"零值正常状态"。详见 §隐含约定 P7。 \ No newline at end of file