--- id: "INTEGRACION-METATRADER4" title: "Integracion MetaTrader4 via MetaAPI" type: "Documentation" project: "trading-platform" version: "1.0.0" updated_date: "2026-01-04" --- # Integracion MetaTrader4 via MetaAPI **Version:** 1.0.0 **Fecha:** 2025-12-08 **Modulo:** Trading Operations **Autor:** Trading Strategist - Trading Platform --- ## Tabla de Contenidos 1. [Vision General](#vision-general) 2. [Arquitectura de Integracion](#arquitectura-de-integracion) 3. [MetaAPI Setup](#metaapi-setup) 4. [Account Management](#account-management) 5. [Trade Execution](#trade-execution) 6. [Price Adjustments](#price-adjustments) 7. [API Endpoints](#api-endpoints) 8. [Error Handling](#error-handling) 9. [Implementacion](#implementacion) 10. [Testing](#testing) --- ## Vision General ### Objetivo Integrar MetaTrader4 (MT4) como broker de ejecucion para: 1. **Conectar cuentas MT4** de diferentes brokers 2. **Ejecutar operaciones** (open, modify, close) 3. **Monitorear posiciones** en tiempo real 4. **Sincronizar datos** de precios 5. **Gestionar multiples cuentas** ### Por que MetaAPI? | Ventaja | Descripcion | |---------|-------------| | Cloud-based | No requiere MT4 local corriendo | | Multi-broker | Soporta cualquier broker MT4 | | API REST/WS | Facil integracion | | Reliability | 99.9% uptime | | Security | Encriptacion end-to-end | --- ## Arquitectura de Integracion ### Diagrama de Flujo ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ METATRADER4 INTEGRATION ARCHITECTURE │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────┐ │ │ │ Trading Platform │ │ │ │ Trading Service │ │ │ │ │ │ │ │ ┌───────────────┐ │ ┌─────────────────────────────────────────┐ │ │ │ │ Trade Manager │ │ │ MetaAPI Cloud │ │ │ │ │ │ │ │ │ │ │ │ │ - Open/Close │◄─┼────▶│ ┌──────────┐ ┌──────────┐ │ │ │ │ │ - Modify SL/TP│ │ │ │ Account │ │ Trading │ │ │ │ │ │ - Get Status │ │ │ │ Manager │ │ API │ │ │ │ │ └───────────────┘ │ │ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ │ │ │ │ │ ┌───────────────┐ │ │ │ │ │ │ │ │ │Price Adjuster │ │ │ ┌────▼─────────────▼────┐ │ │ │ │ │ │ │ │ │ MetaAPI Server │ │ │ │ │ │ - Broker diff │ │ │ │ (Connection Pool) │ │ │ │ │ │ - Slippage │ │ │ └───────────┬───────────┘ │ │ │ │ └───────────────┘ │ │ │ │ │ │ │ │ └──────────────┼──────────────────────────┘ │ │ │ ┌───────────────┐ │ │ │ │ │ │Account Sync │ │ │ │ │ │ │ │ │ │ WebSocket/HTTP │ │ │ │ - Multi-acct │ │ │ │ │ │ │ - Balance │ │ │ │ │ │ └───────────────┘ │ │ │ │ └─────────────────────┘ │ │ │ │ │ │ ┌────────────────────────────────┼────────────────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────┐│ │ │ Broker A │ │ Broker B │ │ Broker C ││ │ │ (IC Markets) │ │ (Pepperstone) │ │ (XM) ││ │ │ │ │ │ │ ││ │ │ ┌────────────┐ │ │ ┌────────────┐ │ │ ┌────┐ ││ │ │ │ MT4 │ │ │ │ MT4 │ │ │ │MT4 │ ││ │ │ │ Terminal │ │ │ │ Terminal │ │ │ │ │ ││ │ │ └────────────┘ │ │ └────────────┘ │ │ └────┘ ││ │ └──────────────────┘ └──────────────────┘ └──────────┘│ │ │ └────────────────────────────────────────────────────────────────────────────┘ ``` ### Componentes 1. **Trade Manager**: Gestiona operaciones CRUD 2. **Price Adjuster**: Compensa diferencias de precios entre brokers 3. **Account Sync**: Sincroniza estado de cuentas 4. **MetaAPI Client**: Wrapper para API de MetaAPI --- ## MetaAPI Setup ### Registro y Credenciales ```bash # 1. Crear cuenta en metaapi.cloud # 2. Obtener API token # 3. Provisionar cuentas MT4 # Environment variables export METAAPI_TOKEN="your-api-token" export METAAPI_DOMAIN="agiliumtrade.agiliumtrade.ai" ``` ### Configuracion ```yaml # config/metaapi_config.yaml metaapi: token: ${METAAPI_TOKEN} domain: agiliumtrade.agiliumtrade.ai accounts: - id: "account-id-1" name: "IC Markets - Main" broker: "ICMarkets" server: "ICMarketsSC-Demo" type: "demo" enabled: true - id: "account-id-2" name: "Pepperstone - Hedge" broker: "Pepperstone" server: "Pepperstone-Demo" type: "demo" enabled: true settings: connection_timeout: 60 # segundos reconnect_attempts: 5 request_timeout: 30 sync_mode: "user" # or "broker" risk: max_slippage_pips: 3 max_spread_pips: 5 requote_retries: 3 ``` ### Provisionamiento de Cuenta ```python # services/metaapi_setup.py from metaapi_cloud_sdk import MetaApi async def provision_account( login: str, password: str, server: str, name: str ) -> dict: """ Provisiona una nueva cuenta MT4 en MetaAPI Args: login: MT4 login number password: MT4 password server: Broker server name name: Friendly name for account Returns: Account configuration dict """ api = MetaApi(token=METAAPI_TOKEN) account = await api.metatrader_account_api.create_account({ 'name': name, 'type': 'cloud', 'login': login, 'password': password, 'server': server, 'platform': 'mt4', 'magic': 123456, # Magic number for our trades 'application': 'Trading PlatformIA', 'keywords': ['trading', 'ml-trading'] }) # Deploy and wait for connection await account.deploy() await account.wait_connected() return { 'id': account.id, 'name': name, 'server': server, 'state': account.state, 'connection_status': account.connection_status } ``` --- ## Account Management ### MetaAPI Client Wrapper ```python # services/metaapi_client.py from metaapi_cloud_sdk import MetaApi from typing import Dict, List, Optional import asyncio class MetaAPIClient: """ Wrapper para MetaAPI con funcionalidades de trading """ def __init__(self, config: Dict): self.config = config self.api = MetaApi(token=config['token']) self.accounts: Dict[str, any] = {} self.connections: Dict[str, any] = {} async def initialize(self): """Inicializa conexiones a todas las cuentas""" for acc_config in self.config['accounts']: if acc_config['enabled']: await self.connect_account(acc_config['id']) async def connect_account(self, account_id: str): """Conecta a una cuenta MT4""" try: account = await self.api.metatrader_account_api.get_account(account_id) # Ensure deployed if account.state != 'DEPLOYED': await account.deploy() # Wait for connection await account.wait_connected() # Get streaming connection connection = account.get_streaming_connection() await connection.connect() await connection.wait_synchronized() self.accounts[account_id] = account self.connections[account_id] = connection return { 'account_id': account_id, 'state': account.state, 'connected': True } except Exception as e: return { 'account_id': account_id, 'connected': False, 'error': str(e) } async def get_account_info(self, account_id: str) -> Dict: """Obtiene informacion de la cuenta""" connection = self.connections.get(account_id) if not connection: raise ValueError(f"Account {account_id} not connected") info = connection.terminal_state.account_information return { 'account_id': account_id, 'broker': info.broker, 'balance': info.balance, 'equity': info.equity, 'margin': info.margin, 'free_margin': info.free_margin, 'margin_level': info.margin_level, 'leverage': info.leverage, 'currency': info.currency } async def get_positions(self, account_id: str) -> List[Dict]: """Obtiene posiciones abiertas""" connection = self.connections.get(account_id) if not connection: raise ValueError(f"Account {account_id} not connected") positions = connection.terminal_state.positions return [{ 'id': p.id, 'symbol': p.symbol, 'type': p.type, # POSITION_TYPE_BUY or POSITION_TYPE_SELL 'volume': p.volume, 'open_price': p.open_price, 'current_price': p.current_price, 'profit': p.profit, 'swap': p.swap, 'commission': p.commission, 'stop_loss': p.stop_loss, 'take_profit': p.take_profit, 'time': p.time, 'magic': p.magic } for p in positions] async def get_orders(self, account_id: str) -> List[Dict]: """Obtiene ordenes pendientes""" connection = self.connections.get(account_id) if not connection: raise ValueError(f"Account {account_id} not connected") orders = connection.terminal_state.orders return [{ 'id': o.id, 'symbol': o.symbol, 'type': o.type, 'volume': o.volume, 'open_price': o.open_price, 'stop_loss': o.stop_loss, 'take_profit': o.take_profit, 'time': o.time, 'expiration': o.expiration_time } for o in orders] async def get_symbol_price(self, account_id: str, symbol: str) -> Dict: """Obtiene precio actual de un simbolo""" connection = self.connections.get(account_id) if not connection: raise ValueError(f"Account {account_id} not connected") price = connection.terminal_state.price(symbol) return { 'symbol': symbol, 'bid': price.bid, 'ask': price.ask, 'spread': price.ask - price.bid, 'time': price.time } async def close(self): """Cierra todas las conexiones""" for connection in self.connections.values(): await connection.close() ``` --- ## Trade Execution ### Trade Manager ```python # services/trade_manager.py from typing import Dict, Optional from dataclasses import dataclass from enum import Enum class OrderType(Enum): MARKET_BUY = "ORDER_TYPE_BUY" MARKET_SELL = "ORDER_TYPE_SELL" LIMIT_BUY = "ORDER_TYPE_BUY_LIMIT" LIMIT_SELL = "ORDER_TYPE_SELL_LIMIT" STOP_BUY = "ORDER_TYPE_BUY_STOP" STOP_SELL = "ORDER_TYPE_SELL_STOP" @dataclass class TradeRequest: symbol: str action: str # "BUY" or "SELL" volume: float stop_loss: float take_profit: float comment: Optional[str] = None magic: int = 123456 @dataclass class TradeResult: success: bool order_id: Optional[str] = None position_id: Optional[str] = None executed_price: Optional[float] = None executed_volume: Optional[float] = None slippage: Optional[float] = None error: Optional[str] = None class TradeManager: """ Gestiona ejecucion de trades via MetaAPI """ def __init__(self, metaapi_client: MetaAPIClient, config: Dict): self.client = metaapi_client self.config = config self.price_adjuster = PriceAdjuster(config.get('price_adjustments', {})) async def execute_market_order( self, account_id: str, request: TradeRequest ) -> TradeResult: """ Ejecuta orden de mercado Args: account_id: ID de cuenta MetaAPI request: TradeRequest con detalles de la orden Returns: TradeResult con resultado de ejecucion """ try: connection = self.client.connections.get(account_id) if not connection: return TradeResult(success=False, error="Account not connected") # Get current price price_info = await self.client.get_symbol_price(account_id, request.symbol) # Adjust prices for broker adjusted_sl, adjusted_tp = self.price_adjuster.adjust_levels( request.symbol, request.action, price_info['bid'] if request.action == 'SELL' else price_info['ask'], request.stop_loss, request.take_profit ) # Check spread if price_info['spread'] > self.config['risk']['max_spread_pips'] * self._get_pip_value(request.symbol): return TradeResult( success=False, error=f"Spread too high: {price_info['spread']}" ) # Execute order if request.action == 'BUY': result = await connection.create_market_buy_order( symbol=request.symbol, volume=request.volume, stop_loss=adjusted_sl, take_profit=adjusted_tp, options={ 'comment': request.comment or 'Trading Platform', 'clientId': f'OQ_{int(time.time())}', 'magic': request.magic, 'slippage': self.config['risk']['max_slippage_pips'] } ) else: result = await connection.create_market_sell_order( symbol=request.symbol, volume=request.volume, stop_loss=adjusted_sl, take_profit=adjusted_tp, options={ 'comment': request.comment or 'Trading Platform', 'clientId': f'OQ_{int(time.time())}', 'magic': request.magic, 'slippage': self.config['risk']['max_slippage_pips'] } ) # Check result if result.string_code == 'TRADE_RETCODE_DONE': expected_price = price_info['ask'] if request.action == 'BUY' else price_info['bid'] slippage = abs(result.price - expected_price) return TradeResult( success=True, order_id=result.order_id, position_id=result.position_id, executed_price=result.price, executed_volume=result.volume, slippage=slippage ) else: return TradeResult( success=False, error=f"{result.string_code}: {result.message}" ) except Exception as e: return TradeResult(success=False, error=str(e)) async def modify_position( self, account_id: str, position_id: str, stop_loss: Optional[float] = None, take_profit: Optional[float] = None ) -> TradeResult: """Modifica SL/TP de una posicion""" try: connection = self.client.connections.get(account_id) if not connection: return TradeResult(success=False, error="Account not connected") result = await connection.modify_position( position_id=position_id, stop_loss=stop_loss, take_profit=take_profit ) if result.string_code == 'TRADE_RETCODE_DONE': return TradeResult(success=True, position_id=position_id) else: return TradeResult( success=False, error=f"{result.string_code}: {result.message}" ) except Exception as e: return TradeResult(success=False, error=str(e)) async def close_position( self, account_id: str, position_id: str, volume: Optional[float] = None # None = close all ) -> TradeResult: """Cierra una posicion (parcial o total)""" try: connection = self.client.connections.get(account_id) if not connection: return TradeResult(success=False, error="Account not connected") if volume: result = await connection.close_position_partially( position_id=position_id, volume=volume ) else: result = await connection.close_position(position_id=position_id) if result.string_code == 'TRADE_RETCODE_DONE': return TradeResult( success=True, position_id=position_id, executed_price=result.price, executed_volume=result.volume ) else: return TradeResult( success=False, error=f"{result.string_code}: {result.message}" ) except Exception as e: return TradeResult(success=False, error=str(e)) async def close_all_positions( self, account_id: str, symbol: Optional[str] = None ) -> List[TradeResult]: """Cierra todas las posiciones (opcionalmente filtradas por simbolo)""" positions = await self.client.get_positions(account_id) if symbol: positions = [p for p in positions if p['symbol'] == symbol] results = [] for position in positions: result = await self.close_position(account_id, position['id']) results.append(result) return results def _get_pip_value(self, symbol: str) -> float: """Obtiene valor del pip para un simbolo""" if 'JPY' in symbol: return 0.01 elif symbol in ['XAUUSD', 'GOLD']: return 0.1 else: return 0.0001 ``` --- ## Price Adjustments ### Manejo de Diferencias de Precio por Broker ```python # services/price_adjuster.py from typing import Dict, Tuple, Optional from dataclasses import dataclass @dataclass class BrokerAdjustment: """Ajustes especificos por broker""" spread_offset: float = 0.0 # Pips de diferencia promedio price_offset: float = 0.0 # Offset de precio sl_buffer: float = 0.0 # Buffer adicional para SL tp_buffer: float = 0.0 # Buffer adicional para TP class PriceAdjuster: """ Ajusta precios para compensar diferencias entre brokers Las diferencias pueden surgir por: - Diferentes proveedores de liquidez - Diferentes markups en spread - Diferencias en feeds de precio - Latencia en actualizacion de precios """ def __init__(self, config: Dict): self.config = config self.adjustments = self._load_adjustments() def _load_adjustments(self) -> Dict[str, BrokerAdjustment]: """Carga ajustes por broker desde config""" adjustments = {} for broker_name, adj_config in self.config.get('brokers', {}).items(): adjustments[broker_name] = BrokerAdjustment( spread_offset=adj_config.get('spread_offset', 0.0), price_offset=adj_config.get('price_offset', 0.0), sl_buffer=adj_config.get('sl_buffer', 0.0), tp_buffer=adj_config.get('tp_buffer', 0.0) ) return adjustments def adjust_levels( self, symbol: str, action: str, entry_price: float, stop_loss: float, take_profit: float, broker: Optional[str] = None ) -> Tuple[float, float]: """ Ajusta SL y TP considerando el broker Args: symbol: Par de trading action: "BUY" o "SELL" entry_price: Precio de entrada stop_loss: Stop loss original take_profit: Take profit original broker: Nombre del broker (opcional) Returns: Tuple con (adjusted_sl, adjusted_tp) """ pip_value = self._get_pip_value(symbol) # Get broker adjustments if broker and broker in self.adjustments: adj = self.adjustments[broker] else: adj = BrokerAdjustment() # Default values # Calculate adjusted levels if action == 'BUY': # For BUY: SL below entry, TP above entry adjusted_sl = stop_loss - (adj.sl_buffer * pip_value) adjusted_tp = take_profit + (adj.tp_buffer * pip_value) else: # For SELL: SL above entry, TP below entry adjusted_sl = stop_loss + (adj.sl_buffer * pip_value) adjusted_tp = take_profit - (adj.tp_buffer * pip_value) return adjusted_sl, adjusted_tp def calculate_real_slippage( self, expected_price: float, executed_price: float, symbol: str ) -> float: """Calcula slippage real en pips""" pip_value = self._get_pip_value(symbol) return abs(executed_price - expected_price) / pip_value def is_price_acceptable( self, expected_price: float, actual_price: float, symbol: str, max_slippage_pips: float = 3.0 ) -> bool: """Verifica si el precio es aceptable""" slippage = self.calculate_real_slippage(expected_price, actual_price, symbol) return slippage <= max_slippage_pips def _get_pip_value(self, symbol: str) -> float: """Obtiene valor del pip""" if 'JPY' in symbol: return 0.01 elif symbol in ['XAUUSD', 'GOLD']: return 0.1 else: return 0.0001 # Configuration example """ price_adjustments: brokers: ICMarkets: spread_offset: 0.2 price_offset: 0.0 sl_buffer: 0.5 # Agregar 0.5 pips al SL tp_buffer: 0.0 Pepperstone: spread_offset: 0.3 price_offset: 0.1 sl_buffer: 0.3 tp_buffer: 0.0 XM: spread_offset: 0.5 price_offset: 0.2 sl_buffer: 1.0 tp_buffer: 0.0 """ ``` --- ## API Endpoints ### FastAPI Trading Service ```python # api/trading_api.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List, Optional app = FastAPI(title="Trading Platform Trading Service") class TradeRequestModel(BaseModel): symbol: str action: str # "BUY" or "SELL" volume: float stop_loss: float take_profit: float comment: Optional[str] = None class ModifyRequestModel(BaseModel): stop_loss: Optional[float] = None take_profit: Optional[float] = None # Dependency injection trade_manager: TradeManager = None @app.on_event("startup") async def startup(): global trade_manager config = load_config() metaapi_client = MetaAPIClient(config['metaapi']) await metaapi_client.initialize() trade_manager = TradeManager(metaapi_client, config) # Account endpoints @app.get("/api/accounts") async def get_accounts(): """Lista todas las cuentas conectadas""" accounts = [] for acc_id in trade_manager.client.accounts: info = await trade_manager.client.get_account_info(acc_id) accounts.append(info) return {"accounts": accounts} @app.get("/api/accounts/{account_id}") async def get_account(account_id: str): """Obtiene informacion de una cuenta""" try: info = await trade_manager.client.get_account_info(account_id) return info except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @app.get("/api/accounts/{account_id}/positions") async def get_positions(account_id: str): """Obtiene posiciones abiertas de una cuenta""" try: positions = await trade_manager.client.get_positions(account_id) return {"positions": positions} except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) @app.get("/api/accounts/{account_id}/orders") async def get_orders(account_id: str): """Obtiene ordenes pendientes""" try: orders = await trade_manager.client.get_orders(account_id) return {"orders": orders} except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) # Trading endpoints @app.post("/api/accounts/{account_id}/trade") async def execute_trade(account_id: str, request: TradeRequestModel): """Ejecuta una operacion de trading""" trade_request = TradeRequest( symbol=request.symbol, action=request.action, volume=request.volume, stop_loss=request.stop_loss, take_profit=request.take_profit, comment=request.comment ) result = await trade_manager.execute_market_order(account_id, trade_request) if not result.success: raise HTTPException(status_code=400, detail=result.error) return { "success": True, "order_id": result.order_id, "position_id": result.position_id, "executed_price": result.executed_price, "slippage": result.slippage } @app.put("/api/accounts/{account_id}/positions/{position_id}") async def modify_position( account_id: str, position_id: str, request: ModifyRequestModel ): """Modifica una posicion existente""" result = await trade_manager.modify_position( account_id, position_id, request.stop_loss, request.take_profit ) if not result.success: raise HTTPException(status_code=400, detail=result.error) return {"success": True, "position_id": position_id} @app.delete("/api/accounts/{account_id}/positions/{position_id}") async def close_position(account_id: str, position_id: str, volume: Optional[float] = None): """Cierra una posicion""" result = await trade_manager.close_position(account_id, position_id, volume) if not result.success: raise HTTPException(status_code=400, detail=result.error) return { "success": True, "position_id": position_id, "executed_price": result.executed_price } @app.delete("/api/accounts/{account_id}/positions") async def close_all_positions(account_id: str, symbol: Optional[str] = None): """Cierra todas las posiciones""" results = await trade_manager.close_all_positions(account_id, symbol) return { "success": all(r.success for r in results), "closed_count": sum(1 for r in results if r.success), "failed_count": sum(1 for r in results if not r.success) } # Market data endpoints @app.get("/api/accounts/{account_id}/price/{symbol}") async def get_price(account_id: str, symbol: str): """Obtiene precio actual de un simbolo""" try: price = await trade_manager.client.get_symbol_price(account_id, symbol) return price except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # Health check @app.get("/api/health") async def health(): connected_accounts = len(trade_manager.client.connections) return { "status": "healthy" if connected_accounts > 0 else "degraded", "connected_accounts": connected_accounts } ``` --- ## Error Handling ### Manejo de Errores Comunes ```python # services/error_handler.py from enum import Enum from typing import Optional import asyncio class MT4Error(Enum): INVALID_STOPS = 10016 INVALID_VOLUME = 10014 MARKET_CLOSED = 10018 NOT_ENOUGH_MONEY = 10019 OFF_QUOTES = 10004 REQUOTE = 10004 TRADE_DISABLED = 10021 NO_CONNECTION = 10006 class TradeErrorHandler: """ Maneja errores de trading con retry logic """ def __init__(self, config: Dict): self.max_retries = config.get('max_retries', 3) self.retry_delay = config.get('retry_delay', 1.0) async def execute_with_retry( self, operation: callable, *args, **kwargs ) -> TradeResult: """ Ejecuta operacion con retry automatico Retries en: - Requotes - Off quotes - Connection issues No retry en: - Invalid stops - Not enough money - Market closed """ last_error = None for attempt in range(self.max_retries): try: result = await operation(*args, **kwargs) if result.success: return result # Check if error is retryable error_code = self._extract_error_code(result.error) if error_code in [MT4Error.REQUOTE, MT4Error.OFF_QUOTES]: last_error = result.error await asyncio.sleep(self.retry_delay * (attempt + 1)) continue elif error_code == MT4Error.NO_CONNECTION: # Try to reconnect await self._try_reconnect() await asyncio.sleep(self.retry_delay * (attempt + 1)) continue else: # Non-retryable error return result except Exception as e: last_error = str(e) await asyncio.sleep(self.retry_delay * (attempt + 1)) return TradeResult( success=False, error=f"Max retries exceeded. Last error: {last_error}" ) def _extract_error_code(self, error_string: str) -> Optional[MT4Error]: """Extrae codigo de error del mensaje""" for error in MT4Error: if str(error.value) in error_string: return error return None async def _try_reconnect(self): """Intenta reconectar a MetaAPI""" # Implementation depends on MetaAPI client pass def format_error_message(self, error: str) -> str: """Formatea mensaje de error para usuario""" error_messages = { MT4Error.INVALID_STOPS: "Stop Loss o Take Profit invalido. Verifica la distancia minima.", MT4Error.INVALID_VOLUME: "Volumen invalido. Verifica el tamano de lote.", MT4Error.MARKET_CLOSED: "El mercado esta cerrado.", MT4Error.NOT_ENOUGH_MONEY: "Margen insuficiente para abrir la posicion.", MT4Error.REQUOTE: "Requote - el precio cambio. Reintentando...", MT4Error.TRADE_DISABLED: "Trading deshabilitado para este simbolo.", } error_code = self._extract_error_code(error) if error_code and error_code in error_messages: return error_messages[error_code] return f"Error de trading: {error}" ``` --- ## Implementacion ### Docker Compose **IMPORTANTE:** Los puertos deben seguir la politica definida en `/core/devtools/environment/DEVENV-PORTS.md` **Puertos asignados a trading-platform:** - Rango base: 3600 - Backend API: 3600 - ML Engine: 3601 - LLM Service: 3602 - Trading Service: 3603 - Database: 5438 (o 5432 compartido) - Redis: 6385 ```yaml # docker-compose.trading.yaml version: '3.8' services: trading-service: build: context: . dockerfile: Dockerfile.trading container_name: trading-trading ports: - "3603:3603" # Trading service (base 3600 + 3) environment: - METAAPI_TOKEN=${METAAPI_TOKEN} - ML_ENGINE_URL=http://ml-engine:3601 - REDIS_URL=redis://redis:6385 - DATABASE_URL=postgresql://trading_user:trading_dev_2025@postgres:5438/trading_data volumes: - ./config:/app/config - ./logs:/app/logs depends_on: - redis - postgres restart: unless-stopped volumes: trading_logs: ``` --- ## Testing ### Test Suite ```python # tests/test_trading.py import pytest from unittest.mock import AsyncMock, patch @pytest.fixture def trade_manager(): config = { 'risk': { 'max_slippage_pips': 3, 'max_spread_pips': 5 } } client = AsyncMock() return TradeManager(client, config) @pytest.mark.asyncio async def test_execute_buy_order(trade_manager): """Test BUY order execution""" # Mock price trade_manager.client.get_symbol_price = AsyncMock(return_value={ 'symbol': 'XAUUSD', 'bid': 2650.00, 'ask': 2650.50, 'spread': 0.50 }) # Mock execution trade_manager.client.connections['test_account'] = AsyncMock() trade_manager.client.connections['test_account'].create_market_buy_order = AsyncMock( return_value=AsyncMock( string_code='TRADE_RETCODE_DONE', order_id='123456', position_id='654321', price=2650.50, volume=0.1 ) ) request = TradeRequest( symbol='XAUUSD', action='BUY', volume=0.1, stop_loss=2645.00, take_profit=2660.00 ) result = await trade_manager.execute_market_order('test_account', request) assert result.success assert result.order_id == '123456' assert result.executed_price == 2650.50 @pytest.mark.asyncio async def test_spread_too_high(trade_manager): """Test rejection when spread is too high""" trade_manager.client.get_symbol_price = AsyncMock(return_value={ 'symbol': 'XAUUSD', 'bid': 2650.00, 'ask': 2652.00, # 20 pips spread 'spread': 2.00 }) request = TradeRequest( symbol='XAUUSD', action='BUY', volume=0.1, stop_loss=2645.00, take_profit=2660.00 ) result = await trade_manager.execute_market_order('test_account', request) assert not result.success assert 'Spread too high' in result.error ``` --- **Documento Generado:** 2025-12-08 **Trading Strategist - Trading Platform**