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
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>
547 lines
15 KiB
Python
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"
|
|
)
|