添加tick推送,增加订单操作功能

This commit is contained in:
2026-07-08 21:17:02 +08:00
parent f29f4e94cb
commit 39deb5f7ba
25 changed files with 1756 additions and 294 deletions
+403 -18
View File
@@ -11,6 +11,10 @@
| 地址 | `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 更干净。
---
@@ -42,7 +46,34 @@ print(api("/health"))
## API 接口速查
### 1. 健康检查
| 分类 | 方法 | 端点 | 用途 |
|------|------|------|------|
| **系统** | `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 保持连接。
---
## 1. 健康检查
```
GET /health
@@ -55,7 +86,7 @@ status = api("/health")
---
### 2. 账户信息
## 2. 账户信息
```
GET /account
@@ -86,12 +117,16 @@ print(f"杠杆: 1:{acc['leverage']}, 币种: {acc['currency']}")
---
### 3. 实时行情
## 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]
@@ -111,14 +146,145 @@ print(f"XAUUSD Bid: {bid} Ask: {ask} Spread: {ask - bid}")
| 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. 品种信息
## 4. 品种信息
```
GET /symbols/{symbol}
```
bid/ask 从实时 tick 数据获取(自动等待最多 3 秒),避免品种刚加入 Market Watch 时返回 0 的问题。
```python
def get_symbol_info(symbol):
info = api(f"/symbols/{symbol}")["data"][0]
@@ -131,7 +297,7 @@ def get_symbol_info(symbol):
---
### 5. 历史 K 线(按偏移量)
## 5. 历史 K 线(按偏移量)
```
GET /rates/from-pos?symbol={symbol}&timeframe={timeframe}&start_pos={start}&count={count}
@@ -203,15 +369,17 @@ print(df.head())
---
### 6. 当前持仓
## 6. 当前持仓
```
GET /positions
GET /positions[?symbol={symbol}]
```
symbol 可选过滤。
```python
def get_positions():
return api("/positions")["data"]
def get_positions(symbol=None):
return api("/positions", params={"symbol": symbol} if symbol else None)["data"]
positions = get_positions()
for pos in positions:
@@ -222,9 +390,132 @@ for pos in positions:
**返回字段:** `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. 挂单
## 7. 挂单
```
GET /orders?symbol={symbol}
@@ -238,9 +529,43 @@ 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. 订单预检
## 8. 订单预检
```
POST /order/check
@@ -248,6 +573,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=""):
"""预检订单"""
@@ -261,7 +588,8 @@ def check_order(symbol, volume, order_type, price, sl=None, tp=None, magic=0, co
"tp": tp or 0,
"magic": magic,
"comment": comment,
"deviation": 10
"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']}")
@@ -274,14 +602,26 @@ def check_order(symbol, volume, order_type, price, sl=None, tp=None, magic=0, co
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. 下单
## 9. 下单
```
POST /order/send
```
`/order/check` 相同,`type_filling` 会自动适配经纪商支持的填充模式。
```python
def send_order(symbol, volume, order_type, price, sl=None, tp=None, magic=0, comment=""):
"""下单"""
@@ -296,7 +636,8 @@ def send_order(symbol, volume, order_type, price, sl=None, tp=None, magic=0, com
"tp": tp or 0,
"magic": magic,
"comment": comment,
"deviation": 10
"deviation": 10,
"type_filling": 0 # 自动适配,不传也行
}
}
result = api_post("/order/send", data)["data"]
@@ -320,7 +661,7 @@ result = send_order("XAUUSD", 0.01, 0, 4180.0, sl=4170.0, tp=4190.0)
---
### 10. 历史成交
## 10. 历史成交
```
GET /history/deals?date_from={from}&date_to={to}&symbol={symbol}
@@ -340,7 +681,7 @@ for d in deals:
---
### 11. 全局变量(MQL5 信号桥)
## 11. 全局变量(MQL5 信号桥)
> 让 MQL5 指标/EA 把信号写出来,Python 通过 Bridge 读取。不用翻译 MQL5 代码。
@@ -468,6 +809,25 @@ class Mt5Bridge:
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]
@@ -490,12 +850,37 @@ class Mt5Bridge:
return self._get("/account")["data"][0]
# ── 持仓 ──
def positions(self):
return self._get("/positions")["data"]
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)
+700 -201
View File
File diff suppressed because it is too large Load Diff
+193 -6
View File
@@ -167,7 +167,7 @@ Start-ScheduledTask -TaskName "Mt5Bridge"
说明:
- 仅更新 `Mt5Bridge.dll` / `Mt5Bridge.exe` 这一侧,就会影响 HTTP API、健康检查、参数校验、错误处理、重连逻辑等桥接服务行为。
- `Alpha Trend.ex5` 不属于桥接服务本体,不替换也不会影响 `/health``/account``/symbols``/rates/from-pos``/positions``/orders``/history/deals``/gvar``/order/check``/order/send` 这些 API 的正常工作。
- `Alpha Trend.ex5` 不属于桥接服务本体,不替换也不会影响 `/health``/account``/symbols``/rates/from-pos``/positions``/orders``/history/deals``/gvar``/order/check``/order/send``/stream/ticks/{symbol}``/stream/ticks-sse/{symbol}` 这些 API 的正常工作。
- 只有在你需要新版指标导出逻辑时,才需要同步替换 `Alpha Trend.ex5`。新版指标变化包括:只输出已收盘 K 线信号,以及使用带周期/参数作用域的 GlobalVariable 名称。
---
@@ -310,6 +310,8 @@ GET /account
GET /symbols/{symbol}
```
bid/ask 从实时 tick 数据获取(自动等待最多 3 秒),避免品种刚加入 Market Watch 时返回 0 的问题。
**示例:** `/symbols/XAUUSDc`
**响应字段:**
@@ -338,6 +340,8 @@ GET /symbols/{symbol}
GET /symbols/{symbol}/tick
```
自动将品种加入 MT5 Market Watch,并等待最多 3 秒获取真实 tick 数据(解决品种未订阅时返回空值的问题)。
**示例:** `/symbols/XAUUSDc/tick`
**响应字段:**
@@ -352,6 +356,79 @@ GET /symbols/{symbol}/tick
| time_msc | string | 毫秒级时间 |
| volume_real | number | 真实成交量 |
### 实时 Tick 推送(WebSocket
```
WS /stream/ticks/{symbol}
```
MT5 每收到一条 tick 就立即推给所有订阅者,避免轮询开销。
-`X-API-Key` 认证(握手 Header 或 `?key=` query 参数)
- 连接时自动把品种加入 MT5 Market Watch;最后一个订阅者断开时自动移除
- 同一品种可被多个客户端同时订阅,互不影响
- 每个品种最多 50 个订阅者(WS + SSE 共用),超出返回 `429`
- 每 30 秒发一条心跳包,无 tick 时也发,用于穿透 NAT 保持连接
**Python 示例(需 `pip install websocket-client`):**
```python
import websocket, json
ws = websocket.WebSocketApp(
"ws://你的IP:13485/stream/ticks/XAUUSDc",
header=["X-API-Key: 你的密码"],
on_message=lambda ws, msg: print(json.loads(msg)),
)
ws.run_forever()
```
**浏览器示例(用 query 参数传 key):**
```js
const ws = new WebSocket("ws://你的IP:13485/stream/ticks/XAUUSDc?key=你的密码");
ws.onmessage = (e) => console.log(JSON.parse(e.data));
```
**推送格式(每条 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
}
```
心跳包:`{"type":"heartbeat","time":"2026-07-08T10:31:15"}`
### 实时 Tick 推送(SSE,不能用 WebSocket 时)
```
GET /stream/ticks-sse/{symbol} → text/event-stream
```
面向无法建 WebSocket 的客户端(部分老浏览器、内网代理、curl 等)。
```bash
curl -N -H "X-API-Key: 你的密码" http://你的IP:13485/stream/ticks-sse/XAUUSDc
```
每条 tick 一帧 SSE
```
data: {"type":"tick","symbol":"XAUUSDc","bid":4180.0,...}
data: {"type":"heartbeat","time":"2026-07-08T10:31:15"}
```
### 历史 K 线
```
@@ -385,9 +462,11 @@ GET /rates/from-pos?symbol={symbol}&timeframe={timeframe}&start_pos={start}&coun
### 当前持仓
```
GET /positions
GET /positions[?symbol={symbol}]
```
symbol 为可选过滤参数。
**响应字段:**
| 字段 | 类型 | 说明 |
@@ -405,6 +484,76 @@ GET /positions
| comment | string | 注释 |
| magic | number | Magic Number |
### 平仓
```
POST /position/close
```
```json
{ "ticket": 12345678 }
{ "ticket": 12345678, "volume": 0.05 } // 部分平仓
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| ticket | number | ✅ | 持仓编号 |
| volume | number | ❌ | 平仓手数;不传/0=全平,>0=部分平仓 |
| deviation | number | ❌ | 允许滑点(默认 10) |
### 改持仓 SL/TP
```
POST /position/modify
```
```json
{ "ticket": 12345678, "sl": 4170.0, "tp": 4190.0 }
```
`sl`/`tp` 设为 `0` 表示清除。
### 对冲平仓(节省点差)
```
POST /position/close-by
```
```json
{ "position": 111, "position_by": 222 }
```
两张持仓必须 **同品种 + 反向**。MT5 用净额结算,只收一次点差。
### 批量平仓
```
POST /positions/close-batch
```
```json
{ "magic": 123456 }
{ "symbol": "XAUUSDc", "magic": 123456 }
```
`symbol``magic` **至少传一个**。返回 `{closed, failed, data:[...]}`
### 移动止损
服务端不做轮询,由客户端结合 `/positions` 拉取和 `/position/modify` 实现:
```python
while True:
for p in bridge.positions():
cur = p["price_current"]
new_sl = cur - 50 if p["type"] == 0 else cur + 50
if abs(new_sl - p["sl"]) >= 10:
bridge.modify_position(p["ticket"], sl=new_sl)
time.sleep(1)
```
也可订阅 WebSocket tick 流(`/stream/ticks/{symbol}`),按 tick 回调触发,更实时。
### 挂单
```
@@ -413,12 +562,34 @@ GET /orders?symbol={symbol}
symbol 为可选参数,不传则返回所有挂单。
### 撤单
```
POST /order/cancel
```
```json
{ "ticket": 87654321 }
```
### 改挂单
```
POST /order/modify
```
```json
{ "ticket": 87654321, "price": 4180.0, "sl": 4170.0, "tp": 4190.0 }
```
### 订单预检
```
POST /order/check
```
**填充模式自动适配**:服务端会根据品种的 `SYMBOL_FILLING_MODE` 自动选择经纪商支持的填充模式。如果请求的 `type_filling` 不被支持,会按 IOC(1) → FOK(0) → RETURN(2) 顺序降级,无需客户端手动判断。
**请求体:**
```json
{
@@ -431,7 +602,8 @@ POST /order/check
"tp": 4190.0,
"magic": 123456,
"comment": "test",
"deviation": 10
"deviation": 10,
"type_filling": 0
}
```
@@ -447,6 +619,17 @@ POST /order/check
| magic | number | Magic Number |
| comment | string | 注释 |
| deviation | number | 偏差 |
| type_filling | number | 填充模式:0=FOK, 1=IOC, 2=RETURN(自动适配,不传也行) |
**type_filling 说明:**
| 值 | 含义 | 说明 |
|------|------|------|
| 0 | FOK (Fill or Kill) | 必须全部成交,否则取消 |
| 1 | IOC (Immediate or Cancel) | 能成交多少成交多少 |
| 2 | RETURN | 剩余部分留在订单簿 |
> 不同经纪商支持的填充模式不同(如 ICMarkets 只支持 IOC),服务端会自动降级,客户端无需关心。
### 下单
@@ -454,6 +637,8 @@ POST /order/check
POST /order/send
```
`/order/check` 相同,`type_filling` 会自动适配经纪商支持的填充模式。
**请求体:**
```json
{
@@ -467,7 +652,8 @@ POST /order/send
"tp": 4190.0,
"magic": 123456,
"comment": "test",
"deviation": 10
"deviation": 10,
"type_filling": 0
}
}
```
@@ -609,7 +795,8 @@ resp = requests.post(f"{BRIDGE}/order/send", headers=HEADERS, json={
"tp": 4190.0,
"magic": 123456,
"comment": "test",
"deviation": 10
"deviation": 10,
"type_filling": 0
}
})
print(resp.json())
@@ -628,7 +815,7 @@ curl -H "X-API-Key: 你的密码" http://IP:端口/account
curl -X POST http://IP:端口/order/send \
-H "Content-Type: application/json" \
-H "X-API-Key: 你的密码" \
-d '{"request":{"action":1,"symbol":"XAUUSDc","volume":0.01,"order_type":0,"price":4180.0,"sl":4170.0,"tp":4190.0,"magic":123456,"comment":"test","deviation":10}}'
-d '{"request":{"action":1,"symbol":"XAUUSDc","volume":0.01,"order_type":0,"price":4180.0,"sl":4170.0,"tp":4190.0,"magic":123456,"comment":"test","deviation":10,"type_filling":0}}'
```
---
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5 -5
View File
@@ -1,17 +1,17 @@
{
"format": 1,
"restore": {
"C:\\Users\\Administrator\\Desktop\\active-fx-traders\\my-Mt5Bridge\\Mt5Bridge.csproj": {}
"C:\\Users\\Administrator\\Desktop\\my-Mt5Bridge-main\\my-mt5bridge\\Mt5Bridge.csproj": {}
},
"projects": {
"C:\\Users\\Administrator\\Desktop\\active-fx-traders\\my-Mt5Bridge\\Mt5Bridge.csproj": {
"C:\\Users\\Administrator\\Desktop\\my-Mt5Bridge-main\\my-mt5bridge\\Mt5Bridge.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "C:\\Users\\Administrator\\Desktop\\active-fx-traders\\my-Mt5Bridge\\Mt5Bridge.csproj",
"projectUniqueName": "C:\\Users\\Administrator\\Desktop\\my-Mt5Bridge-main\\my-mt5bridge\\Mt5Bridge.csproj",
"projectName": "Mt5Bridge",
"projectPath": "C:\\Users\\Administrator\\Desktop\\active-fx-traders\\my-Mt5Bridge\\Mt5Bridge.csproj",
"projectPath": "C:\\Users\\Administrator\\Desktop\\my-Mt5Bridge-main\\my-mt5bridge\\Mt5Bridge.csproj",
"packagesPath": "C:\\Users\\Administrator\\.nuget\\packages\\",
"outputPath": "C:\\Users\\Administrator\\Desktop\\active-fx-traders\\my-Mt5Bridge\\obj\\",
"outputPath": "C:\\Users\\Administrator\\Desktop\\my-Mt5Bridge-main\\my-mt5bridge\\obj\\",
"projectStyle": "PackageReference",
"configFilePaths": [
"C:\\Users\\Administrator\\AppData\\Roaming\\NuGet\\NuGet.Config"
+1 -1
View File
@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Mt5Bridge")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+72a254d088108ba984483d64325ad4a99a15c745")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("Mt5Bridge")]
[assembly: System.Reflection.AssemblyTitleAttribute("Mt5Bridge")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
73a436c0ddfd44461ffa8081e17c16a265a632353ba2c55532dfe1190818c1f9
5bdc0376692346f9b7ab5460cdfbdf4f42e8718c2f108a0051f076dfd3e58d91
@@ -9,11 +9,11 @@ build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = Mt5Bridge
build_property.RootNamespace = Mt5Bridge
build_property.ProjectDir = C:\Users\Administrator\Desktop\active-fx-traders\my-Mt5Bridge\
build_property.ProjectDir = C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.RazorLangVersion = 8.0
build_property.SupportLocalizedComponentNames =
build_property.GenerateRazorMetadataSourceChecksumAttributes =
build_property.MSBuildProjectDirectory = C:\Users\Administrator\Desktop\active-fx-traders\my-Mt5Bridge
build_property.MSBuildProjectDirectory = C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge
build_property._RazorSourceGeneratorDebug =
Binary file not shown.
@@ -1 +1 @@
fa7261daa8f6cd35b2cabdce49d64b4efa111ee76853926074d5c790bfffb9d6
af30adee33ca0803429a173f4f51c3233f7e012d4c5cf604a4e29af73b3bdae7
@@ -26,3 +26,31 @@ C:\Users\Administrator\Desktop\active-fx-traders\my-Mt5Bridge\obj\Release\net8.0
C:\Users\Administrator\Desktop\active-fx-traders\my-Mt5Bridge\obj\Release\net8.0\Mt5Bridge.pdb
C:\Users\Administrator\Desktop\active-fx-traders\my-Mt5Bridge\obj\Release\net8.0\Mt5Bridge.genruntimeconfig.cache
C:\Users\Administrator\Desktop\active-fx-traders\my-Mt5Bridge\obj\Release\net8.0\ref\Mt5Bridge.dll
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\bin\Release\net8.0\Mt5Bridge.exe
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\bin\Release\net8.0\Mt5Bridge.deps.json
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\bin\Release\net8.0\Mt5Bridge.runtimeconfig.json
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\bin\Release\net8.0\Mt5Bridge.dll
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\bin\Release\net8.0\Mt5Bridge.pdb
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\bin\Release\net8.0\MtApi5.dll
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\bin\Release\net8.0\MtClient.dll
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\bin\Release\net8.0\Newtonsoft.Json.dll
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\Mt5Bridge.csproj.AssemblyReference.cache
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\Mt5Bridge.GeneratedMSBuildEditorConfig.editorconfig
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\Mt5Bridge.AssemblyInfoInputs.cache
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\Mt5Bridge.AssemblyInfo.cs
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\Mt5Bridge.csproj.CoreCompileInputs.cache
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\Mt5Bridge.MvcApplicationPartsAssemblyInfo.cache
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\staticwebassets.build.json
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\staticwebassets.development.json
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\staticwebassets\msbuild.Mt5Bridge.Microsoft.AspNetCore.StaticWebAssets.props
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\staticwebassets\msbuild.build.Mt5Bridge.props
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\staticwebassets\msbuild.buildMultiTargeting.Mt5Bridge.props
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\staticwebassets\msbuild.buildTransitive.Mt5Bridge.props
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\staticwebassets.pack.json
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\scopedcss\bundle\Mt5Bridge.styles.css
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\Mt5Bridge.csproj.Up2Date
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\Mt5Bridge.dll
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\refint\Mt5Bridge.dll
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\Mt5Bridge.pdb
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\Mt5Bridge.genruntimeconfig.cache
C:\Users\Administrator\Desktop\my-Mt5Bridge-main\my-mt5bridge\obj\Release\net8.0\ref\Mt5Bridge.dll
Binary file not shown.
@@ -1 +1 @@
78336b0d7c9548c22381256b41e61d95e308b055ebb5bde17d5389da512a3fb0
ad262a91a68b408c9e09c711f30b4c7e0d95eec95987ff49ef173d2adbd218f3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -13,11 +13,11 @@
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "C:\\Users\\Administrator\\Desktop\\active-fx-traders\\my-Mt5Bridge\\Mt5Bridge.csproj",
"projectUniqueName": "C:\\Users\\Administrator\\Desktop\\my-Mt5Bridge-main\\my-mt5bridge\\Mt5Bridge.csproj",
"projectName": "Mt5Bridge",
"projectPath": "C:\\Users\\Administrator\\Desktop\\active-fx-traders\\my-Mt5Bridge\\Mt5Bridge.csproj",
"projectPath": "C:\\Users\\Administrator\\Desktop\\my-Mt5Bridge-main\\my-mt5bridge\\Mt5Bridge.csproj",
"packagesPath": "C:\\Users\\Administrator\\.nuget\\packages\\",
"outputPath": "C:\\Users\\Administrator\\Desktop\\active-fx-traders\\my-Mt5Bridge\\obj\\",
"outputPath": "C:\\Users\\Administrator\\Desktop\\my-Mt5Bridge-main\\my-mt5bridge\\obj\\",
"projectStyle": "PackageReference",
"configFilePaths": [
"C:\\Users\\Administrator\\AppData\\Roaming\\NuGet\\NuGet.Config"
+2 -2
View File
@@ -1,8 +1,8 @@
{
"version": 2,
"dgSpecHash": "G66hRH3mPGs=",
"dgSpecHash": "rCHMtHhZMMQ=",
"success": true,
"projectFilePath": "C:\\Users\\Administrator\\Desktop\\active-fx-traders\\my-Mt5Bridge\\Mt5Bridge.csproj",
"projectFilePath": "C:\\Users\\Administrator\\Desktop\\my-Mt5Bridge-main\\my-mt5bridge\\Mt5Bridge.csproj",
"expectedPackageFiles": [],
"logs": []
}
+416
View File
@@ -0,0 +1,416 @@
#!/usr/bin/env python3
"""
Mt5Bridge end-to-end test.
Usage:
python test_all.py # default remote, read-only + check
python test_all.py --trades # enable trade test (open/close)
python test_all.py --base http://host:p # custom base URL
python test_all.py --symbol XAUUSD # custom symbol
"""
import argparse
import json
import sys
import time
from typing import Callable
try:
import requests
except ImportError:
print("missing requests: pip install requests")
sys.exit(1)
try:
import websocket
except ImportError:
print("missing websocket-client: pip install websocket-client")
sys.exit(1)
# ─────────── Colors ───────────
C_G = "\033[92m"
C_R = "\033[91m"
C_Y = "\033[93m"
C_B = "\033[96m"
C_X = "\033[0m"
C_D = "\033[2m"
# ─────────── Stats ───────────
PASS = 0
FAIL = 0
ERRORS: list[str] = []
def case(name: str, fn: Callable[[], bool]) -> None:
global PASS, FAIL
sys.stdout.write(f"{C_B}- {name}{C_X} ")
try:
ok = fn()
# lambda with walrus expressions returns tuple; take last element
if isinstance(ok, tuple):
ok = ok[-1]
if ok:
print(f"{C_G}PASS{C_X}")
PASS += 1
else:
print(f"{C_R}FAIL{C_X}")
FAIL += 1
ERRORS.append(name)
except AssertionError as e:
print(f"{C_R}FAIL{C_X} {C_D}{e}{C_X}")
FAIL += 1
ERRORS.append(f"{name}: {e}")
except Exception as e:
msg = f"{type(e).__name__}: {e}"
print(f"{C_R}ERROR{C_X} {C_D}{msg}{C_X}")
FAIL += 1
ERRORS.append(f"{name}: {msg}")
# ─────────── HTTP client ───────────
class Bridge:
def __init__(self, base: str, key: str, timeout: float = 15.0):
self.base = base.rstrip("/")
self.key = key
self.timeout = timeout
self.s = requests.Session()
self.s.headers.update({"X-API-Key": key})
def get(self, path: str, params: dict | None = None) -> dict:
r = self.s.get(f"{self.base}{path}", params=params, timeout=self.timeout)
r.raise_for_status()
return r.json()
def post(self, path: str, body: dict) -> dict:
r = self.s.post(f"{self.base}{path}", json=body, timeout=self.timeout)
r.raise_for_status()
return r.json()
def delete(self, path: str) -> dict:
r = self.s.delete(f"{self.base}{path}", timeout=self.timeout)
r.raise_for_status()
return r.json()
def expect_status(self, method: str, path: str, expected: int, **kw) -> int:
r = self.s.request(method, f"{self.base}{path}", timeout=self.timeout, **kw)
assert r.status_code == expected, f"HTTP {r.status_code} != {expected}: {r.text[:200]}"
return r.status_code
# ─────────── SSE ───────────
def sse_first(base: str, key: str, symbol: str, timeout_sec: float) -> tuple[bool, str]:
"""Open SSE, wait for first data: line or timeout."""
import urllib.request
url = f"{base}/stream/ticks-sse/{symbol}?key={key}"
req = urllib.request.Request(url, headers={"X-API-Key": key, "Accept": "text/event-stream"})
with urllib.request.urlopen(req, timeout=timeout_sec + 5) as resp:
deadline = time.time() + timeout_sec
while time.time() < deadline:
line = resp.readline()
if not line:
break
line = line.decode("utf-8", "ignore").rstrip("\r\n")
if line.startswith("data: "):
return True, line[6:80]
return False, ""
# ─────────── WebSocket ───────────
def ws_first(base: str, key: str, symbol: str, timeout_sec: float) -> tuple[bool, str]:
ws_url = "ws" + base[4:] if base.startswith("http") else base
ws_url = f"{ws_url}/stream/ticks/{symbol}?key={key}"
deadline = time.time() + timeout_sec
ws = websocket.create_connection(ws_url, timeout=timeout_sec + 5,
header=[f"X-API-Key: {key}"])
try:
ws.settimeout(1.0)
last_type = None
while time.time() < deadline:
try:
opcode, data = ws.recv_data()
except (websocket.WebSocketTimeoutException, TimeoutError):
continue
if opcode in (websocket.ABNF.OPCODE_TEXT,):
text = data.decode("utf-8", "ignore")
if '"type":"tick"' in text:
return True, text[:80]
last_type = "hb"
elif opcode == websocket.ABNF.OPCODE_CLOSE:
break
# fall back: got heartbeat means transport works, but MT5 may need time
return last_type == "hb", f"heartbeat (no tick yet)"
finally:
try:
ws.close()
except Exception:
pass
return False, ""
# ─────────── Warm-up ───────────
def warmup(b: Bridge, symbol: str, timeout_sec: float = 15.0) -> None:
try:
ok, detail = ws_first(b.base, b.key, symbol, timeout_sec)
if "tick" in detail:
print(f"{C_D} [WS warm-up ok, tick received]{C_X}")
elif "heartbeat" in detail:
print(f"{C_D} [WS warm-up: got heartbeat, waiting for MT5 to initialize quote]{C_X}")
time.sleep(3) # give MT5 time to populate symbol data
else:
print(f"{C_D} [WS warm-up failed (no data)]{C_X}")
except Exception as e:
print(f"{C_D} [WS warm-up exception: {e}]{C_X}")
# ════════════════════════════════════════════════════════════════════
def run(args) -> int:
b = Bridge(args.base, args.key)
sym = args.symbol
print()
print(f"{C_Y}=== Mt5Bridge E2E test ==={C_X}")
print(f"Base : {args.base}")
print(f"Symbol : {sym}")
print(f"Trades : {'enabled (will place real orders)' if args.trades else 'disabled (--trades to enable)'}")
print()
# ── system / account ──
case("1. GET /health", lambda: (
r := b.get("/health"),
r["status"] == "healthy" and r["mt5_connected"] is True
))
case("2. GET /account", lambda: (
r := b.get("/account"),
a := r["data"][0],
print(f" login={a['login']} server={a['server']} balance={a['balance']} currency={a['currency']}"),
a["login"] > 0 and a["balance"] >= 0
))
# ── warm-up to add symbol to Market Watch ──
print(f"{C_D}-- warm-up (WS) --{C_X}")
warmup(b, sym, timeout_sec=6)
# ── quote ──
case("3. GET /symbols/" + sym, lambda: (
r := b.get(f"/symbols/{sym}"),
s := r["data"][0],
print(f" bid={s['bid']} ask={s['ask']} digits={s['digits']} point={s['point']}"),
s["bid"] > 0 and s["digits"] > 0
))
case("4. GET /symbols/" + sym + "/tick", lambda: (
r := b.get(f"/symbols/{sym}/tick"),
t := r["data"][0],
print(f" bid={t['bid']} ask={t['ask']} time={t['time']}"),
t["bid"] > 0 and t["ask"] >= t["bid"]
))
# ── rates ──
case("5. GET /rates/from-pos (H1, count=5)", lambda: (
r := b.get("/rates/from-pos", {"symbol": sym, "timeframe": "TIMEFRAME_H1",
"start_pos": 0, "count": 5}),
print(f" count={r['count']} first={r['data'][0]['time']} last={r['data'][-1]['time']}"),
r["count"] == 5
))
case("6. GET /rates/from-date (cross-day 7-7~7-8, H1)", lambda: (
r := b.get("/rates/from-date", {"symbol": sym, "timeframe": "TIMEFRAME_H1",
"date_from": "2026-07-07", "date_to": "2026-07-08"}),
print(f" count={r['count']} first={r['data'][0]['time']}"),
r["count"] > 0
))
case("6b. GET /rates/from-date (same day 7-7~7-7, M1)", lambda: (
r := b.get("/rates/from-date", {"symbol": sym, "timeframe": "TIMEFRAME_M1",
"date_from": "2026-07-07", "date_to": "2026-07-07"}),
print(f" count={r['count']} (bug fix: should be > 0)"),
r["count"] > 0
))
case("6c. GET /rates/from-date (date_from > date_to, expect 400)", lambda: (
b.expect_status("GET", "/rates/from-date", 400,
params={"symbol": sym, "timeframe": "TIMEFRAME_H1",
"date_from": "2026-07-08", "date_to": "2026-07-01"}),
print(" status=400 ok"),
True
))
# ── positions / orders / history ──
case("7. GET /positions", lambda: (
r := b.get("/positions"),
print(f" count={r['count']}"),
True
))
case("7b. GET /positions?symbol=__NOPE__", lambda: (
r := b.get("/positions", {"symbol": "__NOPE__"}),
print(f" count={r['count']}"),
r["count"] == 0
))
case("8. GET /orders", lambda: (
r := b.get("/orders"),
print(f" count={r['count']}"),
True
))
case("9. GET /history/deals (7-1~7-8)", lambda: (
r := b.get("/history/deals", {"date_from": "2026-07-01", "date_to": "2026-07-08",
"symbol": sym}),
print(f" count={r['count']}"),
True
))
# ── gvar ──
import uuid
gvar = f"TEST_{uuid.uuid4().hex[:8]}"
case(f"10a. POST /gvar/{gvar}", lambda: (
r := b.post(f"/gvar/{gvar}", {"value": 42.5}),
print(f" value={r['value']}"),
r["value"] == 42.5
))
case(f"10b. GET /gvar/{gvar}", lambda: (
r := b.get(f"/gvar/{gvar}"),
print(f" value={r['value']}"),
r["value"] == 42.5
))
case("10c. GET /gvar (expect at least 1 TEST_*)", lambda: (
r := b.get("/gvar"),
found := sum(1 for g in r["data"] if g["name"].startswith("TEST_")),
print(f" total={r['count']} TEST_*={found}"),
found >= 1
))
case(f"10d. DELETE /gvar/{gvar}", lambda: (
r := b.delete(f"/gvar/{gvar}"),
print(f" deleted={r['deleted']}"),
r["deleted"] == gvar
))
# ── order check ──
case("11. POST /order/check (buy 0.01 " + sym + ")", lambda: (
bid := b.get(f"/symbols/{sym}/tick")["data"][0]["bid"],
r := b.post("/order/check", {
"action": 1, "symbol": sym, "volume": 0.01, "order_type": 0,
"price": bid, "sl": 0, "tp": 0, "magic": 0, "comment": "test", "deviation": 10,
"type_filling": 0, # ORDER_FILLING_FOK - required by ICMarkets
}),
rc := r["data"]["retcode"],
print(f" retcode={rc} comment={r['data']['comment']}"),
rc == 0 # with type_filling set, should be 0
))
# ── SSE ──
case(f"12. SSE /stream/ticks-sse/{sym} (<=35s for tick or heartbeat)", lambda: (
result := sse_first(b.base, b.key, sym, timeout_sec=35),
ok := result[0],
snippet := result[1],
print(f" {snippet}"),
ok
))
# ── WebSocket ──
case(f"13. WS /stream/ticks/{sym} (<=35s for tick or heartbeat)", lambda: (
result := ws_first(b.base, b.key, sym, timeout_sec=35),
ok := result[0],
snippet := result[1],
print(f" {snippet}"),
ok
))
# ── trades (opt-in) ──
if args.trades:
print()
print(f"{C_Y}[!] TRADES enabled - will place real orders{C_X}")
print()
case("14. POST /order/send -> /position/close (open 0.01 then close)", lambda: (
bid := b.get(f"/symbols/{sym}/tick")["data"][0]["bid"],
r := b.post("/order/send", {"request": {
"action": 1, "symbol": sym, "volume": 0.01, "order_type": 0,
"price": bid, "sl": 0, "tp": 0, "magic": 99001,
"comment": "selftest", "deviation": 10, "type_filling": 0,
}}),
print(f" send retcode={r['data']['retcode']} order={r['data']['order']}"),
r["data"]["retcode"] == 10009,
time.sleep(0.5),
r2 := b.post("/position/close", {"ticket": r["data"]["order"]}),
print(f" close retcode={r2['data']['retcode']}"),
r2["data"]["retcode"] == 10009
))
case("15. POST /position/modify (change SL/TP, then restore)", lambda: (
r := b.get("/positions"),
print(f" positions count={r['count']}"),
(r["count"] == 0) and (print(" [no positions, skip]"), True)[-1]
or (
p := r["data"][0],
orig_sl := p["sl"], orig_tp := p["tp"],
new_sl := round(p["price_current"] - 50, 2),
new_tp := round(p["price_current"] + 50, 2),
r2 := b.post("/position/modify", {"ticket": p["ticket"], "sl": new_sl, "tp": new_tp}),
print(f" modify retcode={r2['data']['retcode']}"),
r2["data"]["retcode"] == 10009,
r3 := b.post("/position/modify", {"ticket": p["ticket"], "sl": orig_sl, "tp": orig_tp}),
print(f" restore retcode={r3['data']['retcode']}"),
r3["data"]["retcode"] == 10009,
)[-1]
))
case("16. POST /positions/close-batch (magic=99001)", lambda: (
r := b.post("/positions/close-batch", {"magic": 99001}),
print(f" closed={r['closed']} failed={r['failed']}"),
True
))
case("17. POST /order/cancel (place pending then cancel)", lambda: (
bid := b.get(f"/symbols/{sym}/tick")["data"][0]["bid"],
pend_price := int(bid * 0.9),
r := b.post("/order/send", {"request": {
"action": 5, "symbol": sym, "volume": 0.01, "order_type": 2,
"price": pend_price, "sl": 0, "tp": 0, "magic": 99002,
"comment": "selftest-pending", "deviation": 0, "type_filling": 0,
}}),
print(f" place retcode={r['data']['retcode']} order={r['data']['order']}"),
r["data"]["retcode"] == 10009,
time.sleep(0.5),
r2 := b.post("/order/cancel", {"ticket": r["data"]["order"]}),
print(f" cancel retcode={r2['data']['retcode']}"),
r2["data"]["retcode"] == 10009
))
else:
print()
print(f"{C_D}[skip trades -- use --trades to enable]{C_X}")
# ── summary ──
print()
print(f"{C_Y}=== Summary ==={C_X}")
print(f"Pass: {C_G}{PASS}{C_X}")
color = C_R if FAIL else C_G
print(f"Fail: {color}{FAIL}{C_X}")
if ERRORS:
print()
print(f"{C_R}Failed cases:{C_X}")
for e in ERRORS:
print(f" - {e}")
print()
return 0 if FAIL == 0 else 1
def main():
p = argparse.ArgumentParser(description="Mt5Bridge end-to-end test")
p.add_argument("--base", default="http://61.164.252.86:13485", help="Bridge base URL")
p.add_argument("--key", default="UiHMqtaYLZzwBdcuS4RFmEGhgDO8N2eI", help="API Key")
p.add_argument("--symbol", default="XAUUSD", help="Test symbol")
p.add_argument("--trades", action="store_true", help="Enable trade test (places real orders)")
args = p.parse_args()
sys.exit(run(args))
if __name__ == "__main__":
main()
-53
View File
@@ -1,53 +0,0 @@
import requests
import time
import statistics
BRIDGE = "http://61.164.252.86:13485"
KEY = "UiHMqtaYLZzwBdcuS4RFmEGhgDO8N2eI"
HEADERS = {"X-API-Key": KEY}
N = 20 # 测试次数
def test(name, func):
times = []
for i in range(N):
t0 = time.perf_counter()
try:
func()
except Exception as e:
print(f"{name} 失败: {e}")
return
t1 = time.perf_counter()
times.append((t1 - t0) * 1000)
print(f" #{i+1:2d}: {times[-1]:.1f} ms", end="\r")
print(f"\n{'='*50}")
print(f"{name}")
print(f" 最小: {min(times):.1f} ms")
print(f" 最大: {max(times):.1f} ms")
print(f" 平均: {statistics.mean(times):.1f} ms")
print(f" 中位: {statistics.median(times):.1f} ms")
print(f"测试 {N} 次,请稍候...\n")
# 1. 健康检查
test("健康检查 /health", lambda: requests.get(f"{BRIDGE}/health", headers=HEADERS).raise_for_status())
# 2. 账户信息
test("账户信息 /account", lambda: requests.get(f"{BRIDGE}/account", headers=HEADERS).raise_for_status())
# 3. 实时 Tick
test("实时 Tick /tick", lambda: requests.get(f"{BRIDGE}/symbols/XAUUSDc/tick", headers=HEADERS).raise_for_status())
# 4. 持仓查询
test("持仓查询 /positions", lambda: requests.get(f"{BRIDGE}/positions", headers=HEADERS).raise_for_status())
# 5. 品种信息
test("品种信息 /symbols", lambda: requests.get(f"{BRIDGE}/symbols/XAUUSDc", headers=HEADERS).raise_for_status())
# 6. K 线数据
test("K 线 /rates (100根 H1)", lambda: requests.get(f"{BRIDGE}/rates/from-pos", headers=HEADERS, params={
"symbol": "XAUUSDc", "timeframe": "TIMEFRAME_H1", "start_pos": 0, "count": 100
}).raise_for_status())
print(f"\n总结: 你的网络 → MT5 云端 往返延迟")