Files
2026-07-11 02:42:55 +08:00

357 lines
14 KiB
Python

#!/usr/bin/env python3
"""
GENESIS — Mt5Bridge Unified Adapter
Provides a bridge() function with the SAME call interface as the old API2TRADE version,
but internally routes all calls to the Mt5Bridge REST API.
Usage:
from core.mt5_bridge import bridge, get_bars, pip_size
acc = bridge("/balance")
pos = bridge("/positions")
q = bridge("/quote?symbol=EURUSD")
ord = bridge("/market", "POST", {"symbol":"EURUSD","type":"Buy","volume":0.1,
"stop_loss":1.08,"take_profit":1.09,"comment":"TEST"})
bridge("/close", "POST", {"ticket": 12345})
bridge("/modify", "POST", {"ticket": 12345, "stop_loss": 1.07})
Environment variables:
MT5_BRIDGE_URL — Base URL (default: http://61.164.252.86:13485)
MT5_BRIDGE_KEY — API Key for X-API-Key header
MT5_SYMBOL_MAP — JSON string mapping xx-suffix symbols to broker symbols
e.g. '{"EURUSDxx":"EURUSD","XAUUSDxx":"XAUUSDc"}'
Mt5Bridge API docs: see Mt5Bridge使用指南.md
"""
import os, json, logging, math
from datetime import datetime, timezone, timedelta
from pathlib import Path
import requests
log = logging.getLogger(__name__)
BRIDGE_URL = os.getenv("MT5_BRIDGE_URL", "http://61.164.252.86:13485")
BRIDGE_KEY = os.getenv("MT5_BRIDGE_KEY", "")
_SYMBOL_MAP_RAW = os.getenv("MT5_SYMBOL_MAP", "")
if _SYMBOL_MAP_RAW:
try:
SYMBOL_MAP = json.loads(_SYMBOL_MAP_RAW)
except json.JSONDecodeError:
SYMBOL_MAP = {}
else:
SYMBOL_MAP = {}
_HEADERS = {"X-API-Key": BRIDGE_KEY, "Content-Type": "application/json"}
def resolve_symbol(sym: str) -> str:
if sym in SYMBOL_MAP:
resolved = SYMBOL_MAP[sym]
if resolved != sym:
log.debug(f"resolve_symbol: {sym}{resolved} (MAP)")
return resolved
if sym.endswith("xx"):
base = sym[:-2]
if base in SYMBOL_MAP:
resolved = SYMBOL_MAP[base]
log.debug(f"resolve_symbol: {sym}{resolved} (MAP via base)")
return resolved
log.debug(f"resolve_symbol: {sym}{base} (strip xx)")
return base
return sym
def _api_get(path: str, params=None) -> dict:
try:
r = requests.get(f"{BRIDGE_URL}{path}", params=params,
headers=_HEADERS, timeout=15)
r.raise_for_status()
return r.json()
except Exception as e:
log.error(f"Mt5Bridge GET {path}: {e}")
return {}
def _api_post(path: str, data: dict) -> dict:
try:
r = requests.post(f"{BRIDGE_URL}{path}", json=data,
headers=_HEADERS, timeout=15)
r.raise_for_status()
return r.json()
except Exception as e:
log.error(f"Mt5Bridge POST {path}: {e}")
return {}
def bridge(path, method="GET", data=None) -> dict:
"""
Unified bridge interface — same signature as the old API2TRADE version.
Supported paths:
/balance → GET /account
/positions → GET /positions
/history → GET /history/deals (today)
/quote?symbol=X → GET /symbols/{sym}/tick
/market (POST) → POST /order/send
/close (POST) → POST /position/close
/modify (POST) → POST /position/modify
/symbols/{sym} → GET /symbols/{sym}
/rates?symbol=X&tf=M5&count=100 → GET /rates/from-pos
"""
# ── Quote ──────────────────────────────────────────────────────────────
if path.startswith("/quote"):
sym = path.split("symbol=")[-1] if "symbol=" in path else ""
if not sym and data:
sym = data.get("symbol", "")
sym = resolve_symbol(sym)
raw = _api_get(f"/symbols/{sym}/tick")
items = raw.get("data", [])
if items:
t = items[0]
return {
"bid": float(t.get("bid", 0)),
"ask": float(t.get("ask", 0)),
"symbol": sym,
}
return {"bid": 0, "ask": 0, "symbol": sym}
# ── Balance / Account ──────────────────────────────────────────────────
if path == "/balance":
raw = _api_get("/account")
items = raw.get("data", [])
if not items:
raw = _api_get("/account")
items = raw.get("data", [])
if items:
a = items[0]
return {
"balance": float(a.get("balance", 0)),
"equity": float(a.get("equity", 0)),
"margin": float(a.get("margin", 0)),
"profit": float(a.get("profit", 0)),
"margin_free": float(a.get("margin_free", 0)),
"margin_level": float(a.get("margin_level", 0)),
"leverage": int(a.get("leverage", 0)),
"currency": a.get("currency", "USD"),
}
return {"balance": 0, "equity": 0, "margin": 0, "profit": 0}
# ── Positions ──────────────────────────────────────────────────────────
if path == "/positions":
sym_filter = None
if data and data.get("symbol"):
sym_filter = resolve_symbol(data["symbol"])
raw = _api_get("/positions", params={"symbol": sym_filter} if sym_filter else None)
items = raw.get("data", [])
return [{
"ticket": p.get("ticket", 0),
"symbol": p.get("symbol", ""),
"orderType": "BUY" if p.get("type", 0) == 0 else "SELL",
"type": p.get("type", 0),
"lots": float(p.get("volume", 0)),
"volume": float(p.get("volume", 0)),
"openPrice": float(p.get("price_open", 0)),
"price_open": float(p.get("price_open", 0)),
"price_current": float(p.get("price_current", 0)),
"sl": float(p.get("sl", 0)),
"tp": float(p.get("tp", 0)),
"profit": float(p.get("profit", 0)),
"swap": float(p.get("swap", 0)),
"comment": p.get("comment", ""),
"magic": p.get("magic", 0),
} for p in items]
# ── History ────────────────────────────────────────────────────────────
if path == "/history":
now = datetime.now(timezone.utc)
date_from = now.strftime("%Y-%m-%d")
date_to = (now + timedelta(days=1)).strftime("%Y-%m-%d")
raw = _api_get("/history/deals", params={
"date_from": date_from,
"date_to": date_to,
})
items = raw.get("data", [])
return [{
"ticket": d.get("ticket", 0),
"symbol": d.get("symbol", ""),
"type": d.get("type", 0),
"entry": d.get("entry", 0),
"volume": float(d.get("volume", 0)),
"price": float(d.get("price", 0)),
"profit": float(d.get("profit", 0)),
"commission": float(d.get("commission", 0)),
"swap": float(d.get("swap", 0)),
"comment": d.get("comment", ""),
"magic": d.get("magic", 0),
"time": d.get("time", ""),
} for d in items]
# ── Place order ────────────────────────────────────────────────────────
if path == "/market" and data:
sym = resolve_symbol(data.get("symbol", ""))
direction = data.get("type", "Buy")
order_type = 0 if direction.lower() in ("buy", "long") else 1
tick_data = _api_get(f"/symbols/{sym}/tick")
tick_items = tick_data.get("data", [])
price = 0
if tick_items:
price = float(tick_items[0].get("ask" if order_type == 0 else "bid", 0))
request_obj = {
"action": 1,
"symbol": sym,
"volume": float(data.get("volume", 0.01)),
"order_type": order_type,
"price": price,
"sl": 0,
"tp": 0,
"magic": int(data.get("magic", 88001)),
"comment": data.get("comment", "GENESIS"),
"deviation": 10,
"type_filling": 0,
}
sl_val = data.get("stop_loss")
tp_val = data.get("take_profit")
if sl_val is not None and float(sl_val) != 0:
request_obj["sl"] = float(sl_val)
if tp_val is not None and float(tp_val) != 0:
request_obj["tp"] = float(tp_val)
payload = {"request": request_obj}
raw = _api_post("/order/send", payload)
resp_data = raw.get("data", raw)
ticket = resp_data.get("order") or resp_data.get("ticket")
retcode = resp_data.get("retcode", 0)
if retcode == 10009 and ticket:
return {"ticket": ticket}
return {"ticket": ticket, "retcode": retcode,
"comment": resp_data.get("comment", "")}
# ── Close position ─────────────────────────────────────────────────────
if path == "/close" and data:
ticket = data.get("ticket")
payload = {"ticket": int(ticket)}
if data.get("volume"):
payload["volume"] = float(data["volume"])
raw = _api_post("/position/close", payload)
resp_data = raw.get("data", raw)
retcode = resp_data.get("retcode", 0)
if retcode == 10009:
return {"message": "ok"}
return {"retcode": retcode, "comment": resp_data.get("comment", "")}
# ── Modify position ────────────────────────────────────────────────────
if path == "/modify" and data:
ticket = data.get("ticket")
payload = {"ticket": int(ticket)}
if data.get("stop_loss") is not None:
payload["sl"] = float(data["stop_loss"])
if data.get("take_profit") is not None:
payload["tp"] = float(data["take_profit"])
raw = _api_post("/position/modify", payload)
resp_data = raw.get("data", raw)
retcode = resp_data.get("retcode", 0)
if retcode == 10009:
return {"ok": True}
return {"retcode": retcode, "comment": resp_data.get("comment", "")}
# ── Symbol info ────────────────────────────────────────────────────────
if path.startswith("/symbols/"):
sym = path.split("/symbols/")[-1].split("?")[0]
sym = resolve_symbol(sym)
raw = _api_get(f"/symbols/{sym}")
items = raw.get("data", [])
return items[0] if items else {}
# ── K-line rates ───────────────────────────────────────────────────────
if path == "/rates" and data:
sym = resolve_symbol(data.get("symbol", ""))
tf = data.get("timeframe", "M5")
count = data.get("count", 100)
raw = _api_get("/rates/from-pos", params={
"symbol": sym,
"timeframe": f"TIMEFRAME_{tf}",
"start_pos": 0,
"count": count,
})
return raw.get("data", [])
# ── Health check ───────────────────────────────────────────────────────
if path == "/health":
return _api_get("/health")
# ── Fallback: pass through to bridge ───────────────────────────────────
if method == "POST" and data:
return _api_post(path, data)
return _api_get(path, params=data if isinstance(data, dict) else None)
def pip_size(symbol: str) -> float:
s = symbol.upper()
if "JPY" in s:
return 0.01
if "XAU" in s or "GOLD" in s:
return 0.1
return 0.0001
def get_bars(symbol: str, tf: str = "M5", count: int = 100) -> list:
"""
Fetch OHLCV bars from Mt5Bridge /rates/from-pos.
tf: M1, M5, M15, M30, H1, H4, D1
Returns list of dicts with: time, open, high, low, close, tick_volume
"""
sym = resolve_symbol(symbol)
raw = _api_get("/rates/from-pos", params={
"symbol": sym,
"timeframe": f"TIMEFRAME_{tf}",
"start_pos": 0,
"count": count,
})
items = raw.get("data", [])
if not items:
return []
import pandas as pd
df = pd.DataFrame(items)
if "time" in df.columns:
df["time"] = pd.to_datetime(df["time"])
df.columns = [c.lower().replace("tick_volume", "volume") for c in df.columns]
return df.to_dict("records")
def get_bars_by_date(symbol: str, tf: str = "H1",
date_from: str = "", date_to: str = "") -> list:
sym = resolve_symbol(symbol)
params = {
"symbol": sym,
"timeframe": f"TIMEFRAME_{tf}",
}
if date_from:
params["date_from"] = date_from
if date_to:
params["date_to"] = date_to
raw = _api_get("/rates/from-date", params=params)
items = raw.get("data", [])
if not items:
return []
import pandas as pd
df = pd.DataFrame(items)
if "time" in df.columns:
df["time"] = pd.to_datetime(df["time"])
df.columns = [c.lower().replace("tick_volume", "volume") for c in df.columns]
return df.to_dict("records")
def calc_lot(equity: float, sl_pips: float, symbol: str, risk_pct: float = 0.01) -> float:
pip_val_per_lot = 10.0
s = symbol.upper()
if "JPY" in s:
pip_val_per_lot = 9.0
if "GBP" in s:
pip_val_per_lot = 12.5
if "XAU" in s or "GOLD" in s:
pip_val_per_lot = 1.0
raw_lot = (equity * risk_pct) / (sl_pips * pip_val_per_lot) if sl_pips > 0 else 0.01
raw_lot = max(0.01, min(raw_lot, 5.0))
return round(round(raw_lot / 0.01) * 0.01, 2)