""" MT4 Gateway Service - Main Application OrbiQuant IA Trading Platform Gateway service que unifica acceso a múltiples terminales MT4, cada uno con su propio agente de trading. """ import os import yaml from pathlib import Path from typing import Dict, List, Optional from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Depends from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from loguru import logger from dotenv import load_dotenv from providers.mt4_bridge_client import MT4BridgeClient, TradeResult # Load environment variables load_dotenv() # ========================================== # Configuration # ========================================== def load_agents_config() -> Dict: """Carga configuración de agentes desde YAML""" config_path = Path(__file__).parent.parent / "config" / "agents.yml" if config_path.exists(): with open(config_path) as f: return yaml.safe_load(f) return {"agents": {}, "global": {}} CONFIG = load_agents_config() AGENTS_CONFIG = CONFIG.get("agents", {}) GLOBAL_CONFIG = CONFIG.get("global", {}) # MT4 Clients registry MT4_CLIENTS: Dict[str, MT4BridgeClient] = {} # ========================================== # Pydantic Models # ========================================== class TradeRequest(BaseModel): """Request para abrir un trade""" symbol: str action: str # "buy" or "sell" lots: float sl: Optional[float] = None tp: Optional[float] = None comment: str = "OrbiQuant" class ModifyRequest(BaseModel): """Request para modificar una posición""" ticket: int sl: Optional[float] = None tp: Optional[float] = None class CloseRequest(BaseModel): """Request para cerrar una posición""" ticket: int lots: Optional[float] = None class AgentSummary(BaseModel): """Resumen de un agente""" agent_id: str name: str status: str balance: float = 0 equity: float = 0 profit: float = 0 open_positions: int = 0 strategy: str = "" class GlobalSummary(BaseModel): """Resumen global de todos los agentes""" total_balance: float total_equity: float total_profit: float total_positions: int agents_online: int agents_offline: int agents: List[AgentSummary] # ========================================== # Lifespan (startup/shutdown) # ========================================== @asynccontextmanager async def lifespan(app: FastAPI): """Inicializa y cierra conexiones MT4""" logger.info("Starting MT4 Gateway Service...") # Initialize MT4 clients for enabled agents for agent_id, config in AGENTS_CONFIG.items(): if config.get("enabled", False): mt4_config = config.get("mt4", {}) try: client = MT4BridgeClient( host=mt4_config.get("host", "localhost"), port=mt4_config.get("port", 8081), auth_token=mt4_config.get("auth_token", "secret") ) MT4_CLIENTS[agent_id] = client logger.info(f"Initialized MT4 client for {agent_id} ({config.get('name')})") except Exception as e: logger.error(f"Failed to initialize {agent_id}: {e}") logger.info(f"MT4 Gateway ready with {len(MT4_CLIENTS)} agents") yield # Cleanup logger.info("Shutting down MT4 Gateway...") for agent_id, client in MT4_CLIENTS.items(): await client.close() logger.info(f"Closed connection for {agent_id}") # ========================================== # FastAPI App # ========================================== app = FastAPI( title="MT4 Gateway Service", description="Gateway para múltiples agentes de trading MT4", version="1.0.0", lifespan=lifespan ) # CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ========================================== # Dependencies # ========================================== def get_mt4_client(agent_id: str) -> MT4BridgeClient: """Obtiene cliente MT4 para un agente""" if agent_id not in MT4_CLIENTS: raise HTTPException(404, f"Agent {agent_id} not found or not enabled") return MT4_CLIENTS[agent_id] def get_agent_config(agent_id: str) -> Dict: """Obtiene configuración de un agente""" if agent_id not in AGENTS_CONFIG: raise HTTPException(404, f"Agent {agent_id} not found") return AGENTS_CONFIG[agent_id] # ========================================== # Health & Status Endpoints # ========================================== @app.get("/health") async def health_check(): """Health check del servicio""" return { "status": "healthy", "agents_configured": len(AGENTS_CONFIG), "agents_active": len(MT4_CLIENTS) } @app.get("/api/status") async def get_status(): """Estado detallado del servicio""" agents_status = [] for agent_id, config in AGENTS_CONFIG.items(): client = MT4_CLIENTS.get(agent_id) is_connected = False if client: try: is_connected = await client.is_connected() except Exception: is_connected = False agents_status.append({ "agent_id": agent_id, "name": config.get("name", "Unknown"), "enabled": config.get("enabled", False), "connected": is_connected, "strategy": config.get("strategy", {}).get("type", "unknown") }) return { "service": "mt4-gateway", "version": "1.0.0", "agents": agents_status } # ========================================== # Agent Management Endpoints # ========================================== @app.get("/api/agents") async def list_agents(): """Lista todos los agentes configurados""" return { agent_id: { "name": config.get("name"), "enabled": config.get("enabled", False), "strategy": config.get("strategy", {}).get("type"), "pairs": config.get("strategy", {}).get("pairs", []), "active": agent_id in MT4_CLIENTS } for agent_id, config in AGENTS_CONFIG.items() } @app.get("/api/agents/summary", response_model=GlobalSummary) async def get_agents_summary(): """Resumen consolidado de todos los agentes""" summary = GlobalSummary( total_balance=0, total_equity=0, total_profit=0, total_positions=0, agents_online=0, agents_offline=0, agents=[] ) for agent_id, config in AGENTS_CONFIG.items(): if not config.get("enabled", False): continue client = MT4_CLIENTS.get(agent_id) agent_summary = AgentSummary( agent_id=agent_id, name=config.get("name", "Unknown"), status="offline", strategy=config.get("strategy", {}).get("type", "unknown") ) if client: try: account = await client.get_account_info() positions = await client.get_positions() agent_summary.status = "online" agent_summary.balance = account.balance agent_summary.equity = account.equity agent_summary.profit = account.profit agent_summary.open_positions = len(positions) summary.total_balance += account.balance summary.total_equity += account.equity summary.total_profit += account.profit summary.total_positions += len(positions) summary.agents_online += 1 except Exception as e: logger.error(f"Error getting account info for {agent_id}: {e}") agent_summary.status = "error" summary.agents_offline += 1 else: summary.agents_offline += 1 summary.agents.append(agent_summary) return summary # ========================================== # Agent-Specific Endpoints # ========================================== @app.get("/api/agents/{agent_id}") async def get_agent_info(agent_id: str): """Información detallada de un agente""" config = get_agent_config(agent_id) client = MT4_CLIENTS.get(agent_id) result = { "agent_id": agent_id, "name": config.get("name"), "enabled": config.get("enabled", False), "config": { "strategy": config.get("strategy"), "risk": config.get("risk"), "trading_hours": config.get("trading_hours") }, "status": "offline" } if client: try: account = await client.get_account_info() result["status"] = "online" result["account"] = { "balance": account.balance, "equity": account.equity, "margin": account.margin, "free_margin": account.free_margin, "profit": account.profit, "leverage": account.leverage, "currency": account.currency } except Exception as e: result["status"] = "error" result["error"] = str(e) return result @app.get("/api/agents/{agent_id}/account") async def get_agent_account(agent_id: str): """Información de cuenta de un agente""" client = get_mt4_client(agent_id) account = await client.get_account_info() return { "agent_id": agent_id, "balance": account.balance, "equity": account.equity, "margin": account.margin, "free_margin": account.free_margin, "margin_level": account.margin_level, "profit": account.profit, "currency": account.currency, "leverage": account.leverage } @app.get("/api/agents/{agent_id}/positions") async def get_agent_positions(agent_id: str): """Posiciones abiertas de un agente""" client = get_mt4_client(agent_id) positions = await client.get_positions() return { "agent_id": agent_id, "count": len(positions), "positions": [ { "ticket": p.ticket, "symbol": p.symbol, "type": p.type, "lots": p.lots, "open_price": p.open_price, "current_price": p.current_price, "stop_loss": p.stop_loss, "take_profit": p.take_profit, "profit": p.profit, "swap": p.swap, "open_time": p.open_time.isoformat(), "comment": p.comment } for p in positions ] } @app.get("/api/agents/{agent_id}/tick/{symbol}") async def get_agent_tick(agent_id: str, symbol: str): """Precio actual de un símbolo para un agente""" client = get_mt4_client(agent_id) tick = await client.get_tick(symbol) return { "agent_id": agent_id, "symbol": symbol, "bid": tick.bid, "ask": tick.ask, "spread": tick.spread, "timestamp": tick.timestamp.isoformat() } # ========================================== # Trading Endpoints # ========================================== @app.post("/api/agents/{agent_id}/trade") async def execute_trade(agent_id: str, request: TradeRequest): """Ejecuta un trade para un agente""" client = get_mt4_client(agent_id) config = get_agent_config(agent_id) # Validar contra configuración de riesgo risk_config = config.get("risk", {}) max_lot = risk_config.get("max_lot_size", 0.1) if request.lots > max_lot: raise HTTPException(400, f"Lot size {request.lots} exceeds max allowed {max_lot}") # Validar par permitido allowed_pairs = config.get("strategy", {}).get("pairs", []) if allowed_pairs and request.symbol not in allowed_pairs: raise HTTPException(400, f"Symbol {request.symbol} not allowed for this agent") # Ejecutar trade result = await client.open_trade( symbol=request.symbol, action=request.action, lots=request.lots, sl=request.sl, tp=request.tp, comment=f"{request.comment}-{config.get('name', agent_id)}" ) if result.success: logger.info(f"Trade executed for {agent_id}: {request.action} {request.lots} {request.symbol}") else: logger.error(f"Trade failed for {agent_id}: {result.message}") return { "agent_id": agent_id, "success": result.success, "ticket": result.ticket, "message": result.message, "error_code": result.error_code } @app.post("/api/agents/{agent_id}/close") async def close_position(agent_id: str, request: CloseRequest): """Cierra una posición de un agente""" client = get_mt4_client(agent_id) result = await client.close_position( ticket=request.ticket, lots=request.lots ) if result.success: logger.info(f"Position {request.ticket} closed for {agent_id}") else: logger.error(f"Close failed for {agent_id}: {result.message}") return { "agent_id": agent_id, "success": result.success, "ticket": request.ticket, "message": result.message } @app.post("/api/agents/{agent_id}/modify") async def modify_position(agent_id: str, request: ModifyRequest): """Modifica SL/TP de una posición""" client = get_mt4_client(agent_id) result = await client.modify_position( ticket=request.ticket, sl=request.sl, tp=request.tp ) return { "agent_id": agent_id, "success": result.success, "ticket": request.ticket, "message": result.message } @app.post("/api/agents/{agent_id}/close-all") async def close_all_positions(agent_id: str, symbol: Optional[str] = None): """Cierra todas las posiciones de un agente""" client = get_mt4_client(agent_id) results = await client.close_all_positions(symbol=symbol) return { "agent_id": agent_id, "closed": sum(1 for r in results if r.success), "failed": sum(1 for r in results if not r.success), "results": [ {"ticket": r.ticket, "success": r.success, "message": r.message} for r in results ] } # ========================================== # Emergency Controls # ========================================== @app.post("/api/emergency/stop-all") async def emergency_stop_all(): """Cierra todas las posiciones de todos los agentes (EMERGENCY)""" logger.warning("EMERGENCY STOP ALL triggered!") results = {} for agent_id, client in MT4_CLIENTS.items(): try: close_results = await client.close_all_positions() results[agent_id] = { "closed": sum(1 for r in close_results if r.success), "failed": sum(1 for r in close_results if not r.success) } except Exception as e: results[agent_id] = {"error": str(e)} return { "action": "emergency_stop_all", "results": results } # ========================================== # Main Entry Point # ========================================== if __name__ == "__main__": import uvicorn host = os.getenv("GATEWAY_HOST", "0.0.0.0") port = int(os.getenv("GATEWAY_PORT", "8090")) logger.info(f"Starting MT4 Gateway on {host}:{port}") uvicorn.run( "main:app", host=host, port=port, reload=True, log_level="info" )