添加tick推送,增加订单操作功能
This commit is contained in:
+403
-18
@@ -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
File diff suppressed because it is too large
Load Diff
+193
-6
@@ -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.
@@ -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"
|
||||
|
||||
@@ -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.
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.
@@ -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"
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -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 云端 往返延迟")
|
||||
Reference in New Issue
Block a user