# ET-MT4-001: WebSocket Integration for MT4 Gateway **Versión:** 1.0.0 **Fecha:** 2026-01-25 **Epic:** OQI-009 - MT4 Trading Gateway **Componente:** Backend + Frontend WebSocket **Estado:** ❌ **NO IMPLEMENTADO** (0% - BLOCKER P0) **Prioridad:** P0 (Feature vendida sin implementar) --- ## Metadata | Campo | Valor | |-------|-------| | **ID** | ET-MT4-001 | | **Tipo** | Especificación Técnica | | **Epic** | OQI-009 | | **Estado Actual** | ❌ BLOCKER - 0% funcional | | **Impacto** | 🔴 CRÍTICO - Feature vendida a clientes | | **Esfuerzo Estimado** | 180 horas (~1 mes, 2 devs) | | **Dependencias** | MT4 Expert Advisor, Backend FastAPI, Frontend React | --- ## 1. Descripción General **MT4 WebSocket Integration** es el sistema de comunicación en tiempo real entre terminales MetaTrader 4 (MT4) y la plataforma trading-platform. Permite visualizar posiciones activas, ejecutar trades, recibir cotizaciones, y sincronizar estado de cuenta en tiempo real. ### Estado Actual: ❌ BLOCKER CRÍTICO ``` Frontend Components: - MT4ConnectionStatus.tsx → 0% funcional (solo stub) - MT4LiveTradesPanel.tsx → 0% NO EXISTE - MT4PositionsManager.tsx → 0% NO EXISTE Backend Services: - MT4 Gateway → 0% NO IMPLEMENTADO - WebSocket Server → 0% NO IMPLEMENTADO - MT4 Expert Advisor (EA) → 0% NO IMPLEMENTADO Total Implementation: 0% ``` **⚠️ IMPACTO COMERCIAL:** Esta feature fue vendida a clientes pero NO está implementada. Es el gap más crítico identificado en la auditoría. --- ## 2. Arquitectura Propuesta ### 2.1 Visión General ``` ┌────────────────────────────────────────────────────────────────────┐ │ MT4 WebSocket Integration Architecture │ ├────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ │ │ │ MT4 Terminal │ (Usuario ejecuta trades en MT4) │ │ │ + EA Plugin │ │ │ └──────┬───────┘ │ │ │ │ │ │ 1. WebSocket Connection (Bidirectional) │ │ │ ws://localhost:8090/mt4/agent_1 │ │ v │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ MT4 Gateway (Python FastAPI) │ │ │ │ │ │ │ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐ │ │ │ │ │ WebSocket │ │ MT4 Parser │ │ Position Manager │ │ │ │ │ │ Server │ │ │ │ │ │ │ │ │ └──────┬──────┘ └──────┬───────┘ └──────┬──────────────┘ │ │ │ │ │ │ │ │ │ │ │ └────────────────┴──────────────────┘ │ │ │ │ │ │ │ │ └──────────────────────────┼──────────────────────────────────────┘ │ │ │ │ │ 2. Forward via WebSocket │ │ │ ws://trading-platform:3082/mt4 │ │ v │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ Backend WebSocket Server (Express/Socket.io) │ │ │ │ │ │ │ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐ │ │ │ │ │ WS Router │ │ Auth Layer │ │ Broadcast Hub │ │ │ │ │ └──────┬──────┘ └──────┬───────┘ └──────┬──────────────┘ │ │ │ │ │ │ │ │ │ │ │ └────────────────┴──────────────────┘ │ │ │ │ │ │ │ │ └──────────────────────────┼──────────────────────────────────────┘ │ │ │ │ │ 3. Real-time updates │ │ │ WS event: mt4_position_update │ │ v │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ Frontend React App │ │ │ │ │ │ │ │ ┌──────────────────┐ ┌─────────────────────────────────┐ │ │ │ │ │ useMT4WebSocket │ │ MT4LiveTradesPanel.tsx │ │ │ │ │ │ (custom hook) │ │ MT4PositionsManager.tsx │ │ │ │ │ │ │ │ MT4ConnectionStatus.tsx │ │ │ │ │ └────────┬─────────┘ └──────────┬──────────────────────┘ │ │ │ │ │ │ │ │ │ │ └───────────────────────┘ │ │ │ │ │ │ │ │ │ v │ │ │ │ ┌──────────────┐ │ │ │ │ │ mt4Store │ (Zustand state) │ │ │ │ └──────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────────┘ ``` ### 2.2 Componentes del Sistema | Componente | Tecnología | Estado | Esfuerzo | |------------|------------|--------|----------| | **MT4 Expert Advisor** | MQL4 | ❌ No existe | 60h | | **MT4 Gateway (Python)** | FastAPI + WebSockets | ❌ No existe | 40h | | **Backend WS Server** | Express + Socket.io | ❌ No existe | 30h | | **Frontend Components** | React + WS hooks | ⚠️ Stubs (0% funcional) | 30h | | **Zustand Store (mt4Store)** | Zustand | ❌ No existe | 10h | | **Tests** | Pytest + Vitest | ❌ No existe | 10h | **Total:** 180 horas --- ## 3. MT4 Expert Advisor (MQL4) ### 3.1 Responsabilidades - Conectar a MT4 Gateway vía WebSocket - Enviar eventos de trades (open, close, modify) - Enviar heartbeat cada 5s (keep-alive) - Recibir comandos remotos (ejecutar trade desde plataforma) ### 3.2 Eventos Enviados al Gateway ```json // Event: position_opened { "type": "position_opened", "timestamp": "2026-01-25T10:30:15Z", "data": { "ticket": 123456, "symbol": "BTCUSD", "type": "buy", "lots": 0.1, "open_price": 89450.00, "stop_loss": 89150.00, "take_profit": 89850.00, "magic_number": 42, "comment": "Manual trade" } } // Event: position_closed { "type": "position_closed", "timestamp": "2026-01-25T11:45:20Z", "data": { "ticket": 123456, "close_price": 89650.00, "profit": 200.00, "commission": -2.50, "swap": 0.00, "net_profit": 197.50 } } // Event: account_update { "type": "account_update", "timestamp": "2026-01-25T10:30:15Z", "data": { "balance": 10000.00, "equity": 10197.50, "margin": 895.00, "margin_free": 9302.50, "margin_level": 1139.27 } } // Event: heartbeat { "type": "heartbeat", "timestamp": "2026-01-25T10:30:15Z", "agent_id": "agent_1", "status": "connected" } ``` ### 3.3 Comandos Recibidos del Gateway ```json // Command: execute_trade { "command": "execute_trade", "request_id": "uuid-1234", "data": { "symbol": "BTCUSD", "type": "buy", "lots": 0.1, "stop_loss": 89150.00, "take_profit": 89850.00, "comment": "From trading-platform" } } // Response: trade_executed { "type": "trade_executed", "request_id": "uuid-1234", "success": true, "data": { "ticket": 123457, "open_price": 89450.00 } } // Command: modify_position { "command": "modify_position", "request_id": "uuid-1235", "data": { "ticket": 123456, "stop_loss": 89200.00, "take_profit": 89900.00 } } // Command: close_position { "command": "close_position", "request_id": "uuid-1236", "data": { "ticket": 123456 } } ``` ### 3.4 Código MQL4 (Pseudocódigo) ```mql4 //+------------------------------------------------------------------+ //| TradingPlatform_EA.mq4 | //| Copyright 2026, Trading Platform | //+------------------------------------------------------------------+ #property strict // WebSocket library (external) #include // Configuration input string GatewayURL = "ws://localhost:8090/mt4/agent_1"; input string AgentID = "agent_1"; input int HeartbeatInterval = 5000; // 5 seconds WebSocket ws; datetime lastHeartbeat = 0; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Connect to MT4 Gateway if (!ws.Connect(GatewayURL)) { Print("Failed to connect to MT4 Gateway"); return INIT_FAILED; } Print("Connected to MT4 Gateway: ", GatewayURL); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { // Send heartbeat every 5 seconds if (TimeCurrent() - lastHeartbeat > HeartbeatInterval / 1000) { SendHeartbeat(); lastHeartbeat = TimeCurrent(); } // Process incoming commands from gateway string command = ws.Receive(); if (StringLen(command) > 0) { ProcessCommand(command); } } //+------------------------------------------------------------------+ //| Trade transaction event | //+------------------------------------------------------------------+ void OnTrade() { // Detect new positions for (int i = 0; i < OrdersTotal(); i++) { if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) { if (OrderOpenTime() > lastPositionCheck) { SendPositionOpened(OrderTicket()); } } } // Detect closed positions for (int i = 0; i < OrdersHistoryTotal(); i++) { if (OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) { if (OrderCloseTime() > lastPositionCheck) { SendPositionClosed(OrderTicket()); } } } lastPositionCheck = TimeCurrent(); } //+------------------------------------------------------------------+ //| Send position opened event | //+------------------------------------------------------------------+ void SendPositionOpened(int ticket) { if (!OrderSelect(ticket, SELECT_BY_TICKET)) return; string json = StringFormat( "{\"type\":\"position_opened\",\"timestamp\":\"%s\",\"data\":{\"ticket\":%d,\"symbol\":\"%s\",\"type\":\"%s\",\"lots\":%.2f,\"open_price\":%.5f,\"stop_loss\":%.5f,\"take_profit\":%.5f}}", TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS), ticket, OrderSymbol(), OrderType() == OP_BUY ? "buy" : "sell", OrderLots(), OrderOpenPrice(), OrderStopLoss(), OrderTakeProfit() ); ws.Send(json); } //+------------------------------------------------------------------+ //| Process command from gateway | //+------------------------------------------------------------------+ void ProcessCommand(string command) { // Parse JSON command string cmdType = ParseJSONString(command, "command"); if (cmdType == "execute_trade") { ExecuteTradeCommand(command); } else if (cmdType == "modify_position") { ModifyPositionCommand(command); } else if (cmdType == "close_position") { ClosePositionCommand(command); } } //+------------------------------------------------------------------+ ``` --- ## 4. MT4 Gateway (Python FastAPI) ### 4.1 Responsabilidades - Recibir conexiones WebSocket de MT4 Expert Advisors - Parsear eventos MQL4 a JSON estructurado - Reenviar eventos a Backend WebSocket Server - Recibir comandos desde Backend y enviarlos a MT4 ### 4.2 Estructura de Proyecto ``` apps/mt4-gateway/ ├── main.py # FastAPI app ├── websocket/ │ ├── __init__.py │ ├── mt4_handler.py # Maneja conexiones MT4 │ ├── backend_forwarder.py # Reenvía a backend │ └── parser.py # Parse MQL4 JSON ├── models/ │ ├── events.py # Pydantic models para eventos │ └── commands.py # Pydantic models para comandos ├── config.py ├── requirements.txt └── tests/ └── test_websocket.py ``` ### 4.3 Código Python (FastAPI) ```python # main.py from fastapi import FastAPI, WebSocket, WebSocketDisconnect from typing import Dict import asyncio import websockets import json app = FastAPI(title="MT4 Gateway") # Active MT4 connections active_connections: Dict[str, WebSocket] = {} # Backend WebSocket URL BACKEND_WS_URL = "ws://localhost:3082/mt4" backend_ws = None @app.on_event("startup") async def startup(): """Connect to backend WebSocket on startup""" global backend_ws try: backend_ws = await websockets.connect(BACKEND_WS_URL) print(f"Connected to backend: {BACKEND_WS_URL}") except Exception as e: print(f"Failed to connect to backend: {e}") @app.websocket("/mt4/{agent_id}") async def mt4_websocket(websocket: WebSocket, agent_id: str): """ Handle WebSocket connection from MT4 Expert Advisor """ await websocket.accept() active_connections[agent_id] = websocket print(f"MT4 Agent {agent_id} connected") try: while True: # Receive event from MT4 data = await websocket.receive_text() event = json.loads(data) print(f"Received from MT4 {agent_id}: {event['type']}") # Add agent_id to event event['agent_id'] = agent_id # Forward to backend if backend_ws: await backend_ws.send(json.dumps(event)) except WebSocketDisconnect: print(f"MT4 Agent {agent_id} disconnected") del active_connections[agent_id] @app.websocket("/commands") async def commands_websocket(websocket: WebSocket): """ Receive commands from backend and forward to MT4 """ await websocket.accept() try: while True: # Receive command from backend data = await websocket.receive_text() command = json.loads(data) agent_id = command.get('agent_id') if agent_id in active_connections: # Forward command to MT4 mt4_ws = active_connections[agent_id] await mt4_ws.send_text(json.dumps(command)) else: print(f"Agent {agent_id} not connected") except WebSocketDisconnect: print("Backend commands channel disconnected") # Health check @app.get("/health") async def health(): return { "status": "ok", "active_agents": len(active_connections), "backend_connected": backend_ws is not None } ``` --- ## 5. Backend WebSocket Server (Express) ### 5.1 Estructura ``` apps/backend/src/websocket/ ├── server.ts # WebSocket server setup ├── handlers/ │ ├── mt4Handler.ts # Handle MT4 events │ ├── authHandler.ts # Authenticate connections │ └── broadcastHandler.ts # Broadcast to clients ├── middleware/ │ └── authMiddleware.ts # JWT validation └── types/ └── mt4Events.ts # TypeScript interfaces ``` ### 5.2 Código TypeScript (Backend) ```typescript // server.ts import { Server } from 'socket.io' import { createServer } from 'http' import express from 'express' import { verifyJWT } from './middleware/authMiddleware' const app = express() const httpServer = createServer(app) const io = new Server(httpServer, { cors: { origin: 'http://localhost:3000' } }) // Namespace for MT4 events const mt4Namespace = io.of('/mt4') mt4Namespace.use((socket, next) => { // Authenticate client (frontend users) const token = socket.handshake.auth.token try { const user = verifyJWT(token) socket.data.user = user next() } catch (error) { next(new Error('Authentication failed')) } }) mt4Namespace.on('connection', (socket) => { console.log(`Client connected: ${socket.data.user.id}`) // Join room based on user ID (to receive only their MT4 events) socket.join(`user_${socket.data.user.id}`) socket.on('disconnect', () => { console.log(`Client disconnected: ${socket.data.user.id}`) }) }) // Receive events from MT4 Gateway and broadcast to frontend const WebSocket = require('ws') const wss = new WebSocket.Server({ port: 3082 }) wss.on('connection', (ws) => { console.log('MT4 Gateway connected') ws.on('message', (message: string) => { const event = JSON.parse(message) console.log(`MT4 Event: ${event.type}`) // Determine which user to send to (based on agent_id → userId mapping) const userId = getU serIdByAgentId(event.agent_id) if (userId) { // Broadcast to specific user's room mt4Namespace.to(`user_${userId}`).emit('mt4_event', event) } }) ws.on('close', () => { console.log('MT4 Gateway disconnected') }) }) httpServer.listen(3082, () => { console.log('WebSocket server running on port 3082') }) ``` --- ## 6. Frontend Components (React) ### 6.1 Custom Hook: useMT4WebSocket ```typescript // hooks/useMT4WebSocket.ts import { useEffect, useRef } from 'react' import { io, Socket } from 'socket.io-client' import { useMT4Store } from '@/stores/mt4.store' import { useAuthStore } from '@/stores/auth.store' export const useMT4WebSocket = () => { const socketRef = useRef(null) const token = useAuthStore(state => state.token) const { addPosition, updatePosition, removePosition, updateAccount } = useMT4Store() useEffect(() => { if (!token) return // Connect to backend WebSocket const socket = io('ws://localhost:3082/mt4', { auth: { token } }) socketRef.current = socket socket.on('connect', () => { console.log('MT4 WebSocket connected') }) socket.on('mt4_event', (event) => { console.log('MT4 Event received:', event) switch (event.type) { case 'position_opened': addPosition(event.data) break case 'position_closed': removePosition(event.data.ticket) break case 'position_modified': updatePosition(event.data.ticket, event.data) break case 'account_update': updateAccount(event.data) break case 'heartbeat': // Update connection status useMT4Store.setState({ lastHeartbeat: new Date(event.timestamp) }) break } }) socket.on('disconnect', () => { console.log('MT4 WebSocket disconnected') }) return () => { socket.disconnect() } }, [token]) return socketRef.current } ``` ### 6.2 Zustand Store: mt4Store ```typescript // stores/mt4.store.ts import { create } from 'zustand' interface MT4Position { ticket: number symbol: string type: 'buy' | 'sell' lots: number open_price: number stop_loss: number take_profit: number profit: number } interface MT4Account { balance: number equity: number margin: number margin_free: number margin_level: number } interface MT4Store { // State positions: MT4Position[] account: MT4Account | null isConnected: boolean lastHeartbeat: Date | null // Actions addPosition: (position: MT4Position) => void updatePosition: (ticket: number, updates: Partial) => void removePosition: (ticket: number) => void updateAccount: (account: MT4Account) => void } export const useMT4Store = create((set) => ({ positions: [], account: null, isConnected: false, lastHeartbeat: null, addPosition: (position) => set((state) => ({ positions: [...state.positions, position] })), updatePosition: (ticket, updates) => set((state) => ({ positions: state.positions.map((p) => p.ticket === ticket ? { ...p, ...updates } : p ) })), removePosition: (ticket) => set((state) => ({ positions: state.positions.filter((p) => p.ticket !== ticket) })), updateAccount: (account) => set({ account }) })) ``` ### 6.3 Component: MT4LiveTradesPanel ```typescript // components/MT4LiveTradesPanel.tsx import React from 'react' import { useMT4WebSocket } from '@/hooks/useMT4WebSocket' import { useMT4Store } from '@/stores/mt4.store' export const MT4LiveTradesPanel: React.FC = () => { useMT4WebSocket() // Initialize WebSocket connection const positions = useMT4Store(state => state.positions) const account = useMT4Store(state => state.account) return (

MT4 Live Trades

{/* Account Summary */} {account && (
Balance

${account.balance.toFixed(2)}

Equity

${account.equity.toFixed(2)}

Margin

${account.margin.toFixed(2)}

Free Margin

${account.margin_free.toFixed(2)}

)} {/* Positions Table */} {positions.map((position) => ( ))} {positions.length === 0 && ( )}
Ticket Symbol Type Lots Open Price S/L T/P Profit
{position.ticket} {position.symbol} {position.type.toUpperCase()} {position.lots.toFixed(2)} {position.open_price.toFixed(5)} {position.stop_loss.toFixed(5)} {position.take_profit.toFixed(5)} = 0 ? 'text-green-500' : 'text-red-500'}> ${position.profit.toFixed(2)}
No active positions
) } ``` --- ## 7. Plan de Implementación (180h) ### Fase 1: MT4 Expert Advisor (60h) - [ ] Investigar librerías WebSocket para MQL4 (10h) - [ ] Implementar conexión WebSocket (15h) - [ ] Implementar eventos (position_opened, closed, etc.) (20h) - [ ] Implementar comandos remotos (execute_trade, etc.) (10h) - [ ] Testing en demo account (5h) ### Fase 2: MT4 Gateway (Python) (40h) - [ ] Setup FastAPI project (5h) - [ ] Implementar WebSocket handler para MT4 (10h) - [ ] Implementar forwarder a backend (10h) - [ ] Parsers y validación de eventos (5h) - [ ] Testing + error handling (10h) ### Fase 3: Backend WebSocket Server (30h) - [ ] Setup Socket.io en Express (5h) - [ ] Implementar MT4 namespace (10h) - [ ] Implementar auth middleware JWT (5h) - [ ] Implementar broadcast logic (5h) - [ ] Testing + integración (5h) ### Fase 4: Frontend Components (30h) - [ ] Crear mt4Store (Zustand) (5h) - [ ] Implementar useMT4WebSocket hook (8h) - [ ] Implementar MT4LiveTradesPanel (8h) - [ ] Implementar MT4PositionsManager (5h) - [ ] Implementar MT4ConnectionStatus (4h) ### Fase 5: Testing E2E (20h) - [ ] Setup test environment (5h) - [ ] Tests MT4 → Gateway (5h) - [ ] Tests Gateway → Backend (5h) - [ ] Tests Backend → Frontend (5h) --- ## 8. Referencias - **Auditoría:** `TASK-002/entregables/analisis/OQI-009/OQI-009-ANALISIS-COMPONENTES.md` - **MetaQuotes MQL4 Reference:** https://docs.mql4.com/ - **Socket.io Documentation:** https://socket.io/docs/ - **FastAPI WebSockets:** https://fastapi.tiangolo.com/advanced/websockets/ --- **Última actualización:** 2026-01-25 **Estado:** BLOCKER P0 - Requiere implementación urgente **Responsable:** Backend Lead + MT4 Specialist