改为自己的mt5api
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user