Complete remaining ET specs identified in INTEGRATION-PLAN: - ET-EDU-007: Video Player Advanced (554 LOC component) - ET-MT4-001: WebSocket Integration (BLOCKER - 0% implemented) - ET-ML-009: Ensemble Signal (Multi-strategy aggregation) - ET-TRD-009: Risk-Based Position Sizer (391 LOC component) - ET-TRD-010: Drawing Tools Persistence (backend + store) - ET-TRD-011: Market Bias Indicator (multi-timeframe analysis) - ET-PFM-009: Custom Charts (SVG AllocationChart + Canvas PerformanceChart) - ET-ML-008: ICT Analysis Card (expanded - 294 LOC component) All specs include: - Architecture diagrams - Complete code examples - API contracts - Implementation guides - Testing scenarios Related: TASK-2026-01-25-002-FRONTEND-COMPREHENSIVE-AUDIT Priority: P1-P3 (mixed) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
28 KiB
28 KiB
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
// 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
// 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)
//+------------------------------------------------------------------+
//| TradingPlatform_EA.mq4 |
//| Copyright 2026, Trading Platform |
//+------------------------------------------------------------------+
#property strict
// WebSocket library (external)
#include <WebSocket.mqh>
// 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)
# 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)
// 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
// 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<Socket | null>(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
// 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<MT4Position>) => void
removePosition: (ticket: number) => void
updateAccount: (account: MT4Account) => void
}
export const useMT4Store = create<MT4Store>((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
// 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 (
<div className="bg-gray-900 rounded-lg p-4">
<h2 className="text-xl font-bold mb-4">MT4 Live Trades</h2>
{/* Account Summary */}
{account && (
<div className="grid grid-cols-4 gap-4 mb-6">
<div>
<span className="text-gray-400">Balance</span>
<p className="text-2xl font-bold">${account.balance.toFixed(2)}</p>
</div>
<div>
<span className="text-gray-400">Equity</span>
<p className="text-2xl font-bold">${account.equity.toFixed(2)}</p>
</div>
<div>
<span className="text-gray-400">Margin</span>
<p className="text-2xl">${account.margin.toFixed(2)}</p>
</div>
<div>
<span className="text-gray-400">Free Margin</span>
<p className="text-2xl">${account.margin_free.toFixed(2)}</p>
</div>
</div>
)}
{/* Positions Table */}
<table className="w-full">
<thead>
<tr className="text-left border-b border-gray-700">
<th>Ticket</th>
<th>Symbol</th>
<th>Type</th>
<th>Lots</th>
<th>Open Price</th>
<th>S/L</th>
<th>T/P</th>
<th>Profit</th>
</tr>
</thead>
<tbody>
{positions.map((position) => (
<tr key={position.ticket} className="border-b border-gray-800">
<td>{position.ticket}</td>
<td className="font-mono">{position.symbol}</td>
<td>
<span className={position.type === 'buy' ? 'text-green-500' : 'text-red-500'}>
{position.type.toUpperCase()}
</span>
</td>
<td>{position.lots.toFixed(2)}</td>
<td>{position.open_price.toFixed(5)}</td>
<td>{position.stop_loss.toFixed(5)}</td>
<td>{position.take_profit.toFixed(5)}</td>
<td className={position.profit >= 0 ? 'text-green-500' : 'text-red-500'}>
${position.profit.toFixed(2)}
</td>
</tr>
))}
{positions.length === 0 && (
<tr>
<td colSpan={8} className="text-center py-8 text-gray-500">
No active positions
</td>
</tr>
)}
</tbody>
</table>
</div>
)
}
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