# 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`。

---

## 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？先去读 [§「⚠️ 隐含约定与已知陷阱」](#-隐含约定与已知陷阱先读)** —— 6 个非显而易见的约定（date_to 排他、`entry` 字段语义反、`/order/send` 不返回 fill 价 等），不读这一节直接调接口几乎必踩。

### 返回 "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`。