workspace/projects/trading-platform/apps/mt4-gateway/src/main.py
rckrdmrd 513a86ceee
Some checks are pending
CI Pipeline / changes (push) Waiting to run
CI Pipeline / core (push) Blocked by required conditions
CI Pipeline / trading-backend (push) Blocked by required conditions
CI Pipeline / trading-data-service (push) Blocked by required conditions
CI Pipeline / trading-frontend (push) Blocked by required conditions
CI Pipeline / erp-core (push) Blocked by required conditions
CI Pipeline / erp-mecanicas (push) Blocked by required conditions
CI Pipeline / gamilit-backend (push) Blocked by required conditions
CI Pipeline / gamilit-frontend (push) Blocked by required conditions
Major update: orchestration system, catalog references, and multi-project enhancements
Core:
- Add catalog reference implementations (auth, payments, notifications, websocket, etc.)
- New agent profiles: Database Auditor, Integration Validator, LLM Agent, Policy Auditor, Trading Strategist
- Update SIMCO directives and add escalation/git guidelines
- Add deployment inventory and audit execution reports

Projects:
- erp-suite: DevOps configs, Dockerfiles, shared libs, vertical enhancements
- gamilit: Test structure, admin controllers, service refactoring, husky/commitlint
- trading-platform: MT4 gateway, auth controllers, admin frontend, deployment scripts
- platform_marketing_content: Full DevOps setup, tests, Docker configs
- betting-analytics/inmobiliaria-analytics: Initial app structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 22:53:55 -06:00

547 lines
15 KiB
Python

"""
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"
)