Files
my-new-linux-mt5-bot/Mt5Bridge使用指南.md
2026-07-11 02:42:55 +08:00

1149 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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&LUSD
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 currencyUSD),不是 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 currencybridge 严格遵循。
**如果手动算 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 currencyUSD),不是 quote currency。详见 §隐含约定 P4。
### 返回 "无法连接到远程服务器"
Bridge 未运行或网络不通,先检查 `/health`
### 业务字段返回默认值 → 怎么区分"无数据"和"真状态"
`/account` 在账户未加载完成时返回 `data:[]` + 全零字段(HTTP 200)。客户端若只看 `bool(response)``field == 0`,会把"无数据"误判成"零值正常状态"。详见 §隐含约定 P7。