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

36 KiB
Raw Permalink Blame History

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/dealsdate_toEXCLUSIVE(不含当天)

# ❌ 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/dealsentry 字段语义跟 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)。

# ✅ 正确:找关仓 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 currencyUSD),不是 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 currencybridge 严格遵循。
如果手动算 USDCAD / USDJPY P&L 时按 quote currency 处理,会差一个汇率倍数USDCAD 大约 1.4×)。

P5. bridge 拿到的 tick 跟 broker 实际 fill 差 1-4 ticks

/positionsprice_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 长度 + 身份字段(如 loginticket)作为"真实数据就绪"的标志:

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 端点。判 positionsordersdeals 同理。


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 认证(Header X-API-Key: your-keyWebSocket 客户端在握手 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.ex5Bridge 仍然可以正常工作,/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 currencyUSD),不是 quote currency。详见 §隐含约定 P4。

返回 "无法连接到远程服务器"

Bridge 未运行或网络不通,先检查 /health

业务字段返回默认值 → 怎么区分"无数据"和"真状态"

/account 在账户未加载完成时返回 data:[] + 全零字段(HTTP 200)。客户端若只看 bool(response)field == 0,会把"无数据"误判成"零值正常状态"。详见 §隐含约定 P7。