#!/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)