35 KiB
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)
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(不含当天)
# ❌ 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)。
# ✅ 正确:找关仓 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
// 实际响应(成功)
{"data": {"retcode": 10009, "order": 1797395084, "comment": "Request executed"}}
bridge 不返回 price 也不返回 deal 字段。所以拿真实 fill 价格和实现 P&L,唯一办法是 close 之后查 /history/deals。
# ❌ 永远拿不到正确价格
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
# 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,不是错误)
客户端很容易把"无数据"误判成"默认值数据"。例:
# ❌ 错: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)作为"真实数据就绪"的标志:
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
status = api("/health")
# {"status": "healthy", "mt5_connected": true, "api_version": "1.0.0"}
2. 账户信息
GET /account
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 数据(解决品种未订阅时返回空值的问题)。
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认证(HeaderX-API-Key: your-key,WebSocket 客户端在握手 Header 里带) - 连上时自动把品种加入 MT5 Market Watch;最后一个订阅者断开时自动移除
- 每个品种最多 50 个订阅者(含 WS + SSE 总数),超出返回
429 - 每 30 秒发一条心跳(无 tick 时也发),客户端可用于保活与断线检测
推送格式(每条 tick 一帧 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
}
心跳包:
{"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 测试:
curl -N -H "X-API-Key: your-api-key" \
http://61.164.252.86:13485/stream/ticks-sse/XAUUSDc
Python SSE 客户端(sseclient-py):
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):
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 版):
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())
浏览器控制台测试:
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 的问题。
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 |
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 |
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 可选过滤。
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
// 全平
{ "ticket": 12345678 }
// 部分平仓
{ "ticket": 12345678, "volume": 0.05 }
| 字段 | 必填 | 说明 |
|---|---|---|
| ticket | ✅ | 持仓编号 |
| volume | ❌ | 平仓手数;不传/0 = 全平;>0 = 部分平仓 |
| deviation | ❌ | 允许滑点(默认 10) |
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
{ "ticket": 12345678, "sl": 4170.0, "tp": 4190.0 }
SL/TP 设为 0 表示清除对应止损/止盈。
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 净额结算),适合双向网格 / 锁仓策略快速离场。
{ "position": 111, "position_by": 222 }
要求:两张持仓 同品种 + 反向。
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 由客户端自己做:
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 批量平仓。至少传一个过滤条件,避免误清整个账户。
{ "magic": 123456 }
{ "symbol": "XAUUSDc", "magic": 123456 }
{ "symbol": "XAUUSDc" }
| 字段 | 必填 | 说明 |
|---|---|---|
| symbol | 一 | 品种过滤 |
| magic | 一 | Magic Number 过滤 |
| deviation | ❌ | 允许滑点(默认 10) |
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 可选,不传返回全部。
orders = api("/orders")["data"]
for o in orders:
print(f"{o['ticket']} {o['symbol']} 类型:{o['type']} 手数:{o['volume_initial']}")
7-1. 撤单
POST /order/cancel
{ "ticket": 87654321 }
def cancel_order(ticket):
return api_post("/order/cancel", {"ticket": ticket})["data"]
cancel_order(87654321)
7-2. 改挂单
POST /order/modify
{ "ticket": 87654321, "price": 4180.0, "sl": 4170.0, "tp": 4190.0 }
sl/tp 设为 0 表示清除。
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) 顺序降级,无需客户端手动判断。
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 会自动适配经纪商支持的填充模式。
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}
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
gvars = api("/gvar")
print(gvars)
# {"data": [{"name": "AT_Trend_XAUUSD", "value": 1}, ...], "count": 5}
读取指定变量
GET /gvar/{name}
trend = api("/gvar/AT_Trend_XAUUSD")["value"]
print(f"趋势方向: {'多头' if trend == 1 else '空头'}")
写入变量
POST /gvar/{name}
Body: {"value": 75.5}
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 的必需步骤。
// 在 OnCalculate 末尾加
int last_closed = rates_total - 2;
string key = StringFormat("MY_SIGNAL_%s_%s", _Symbol, EnumToString(_Period));
GlobalVariableSet(key, signal_value[last_closed]);
第二步: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,则读取方式应继续对应旧键名,例如:
trend = api("/gvar/AT_Trend_XAUUSD")["value"]
buy_signal = api("/gvar/AT_Buy_XAUUSD")["value"]
第三步:根据信号做决策
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("指标发出买入信号,已开多")
完整策略模板
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 快速测试
$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。
与 RaptorBT 策略框架集成
RaptorBT 的 app/main.py 已内置 Mt5Bridge 数据加载,通过环境变量配置连接:
环境变量
| 变量 | 默认 | 说明 |
|---|---|---|
MT5_BRIDGE_URL |
http://61.164.252.86:13485 |
Bridge 服务地址 |
MT5_BRIDGE_KEY |
(内置默认) | API Key |
设置方式
Windows PowerShell:
$env:MT5_BRIDGE_URL = "http://61.164.252.86:13485"
$env:MT5_BRIDGE_KEY = "your-api-key"
Linux/macOS:
export MT5_BRIDGE_URL="http://61.164.252.86:13485"
export MT5_BRIDGE_KEY="your-api-key"
或写入 .env 文件(已被 .gitignore 忽略):
MT5_BRIDGE_URL=http://61.164.252.86:13485
MT5_BRIDGE_KEY=your-api-key
在策略中使用
# 设置环境变量后,直接用 CLI 拉取真实数据回测
python -m app.main run --strategy sma_cross --symbol XAUUSD --timeframe H1 --bars 500
# 优化参数
python -m app.main optimize --strategy sma_cross \
--param fast=5,10,15 --param slow=20,30 --symbol XAUUSD --bars 1000
# 完整交付
python -m app.main deliver --strategy sma_cross \
--param fast=5,10 --param slow=20,30 --symbol XAUUSD --bars 1000
安全提示
- 生产环境务必使用环境变量,不要将 API Key 硬编码到策略文件或提交到版本控制
.env文件已被.gitignore忽略,可安全存放密钥- 若 Key 泄露,联系 Bridge 管理员重置