trading-platform/docs/01-arquitectura/INTEGRACION-METATRADER4.md
rckrdmrd c1b5081208 feat(ml): Complete FASE 11 - BTCUSD update and comprehensive documentation alignment
ML Engine Updates:
- Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records
- Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence)
- Backtest results: +176.71R profit with aggressive_filter strategy

Documentation Consolidation:
- Created docs/99-analisis/_MAP.md index with 13 new analysis documents
- Consolidated inventories: removed duplicates from orchestration/inventarios/
- Updated ML_INVENTORY.yml with BTCUSD metrics and training results
- Added execution reports: FASE11-BTCUSD, correction issues, alignment validation

Architecture & Integration:
- Updated all module documentation with NEXUS v3.4 frontmatter
- Fixed _MAP.md indexes across all folders
- Updated orchestration plans and traces

Files: 229 changed, 5064 insertions(+), 1872 deletions(-)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 09:31:29 -06:00

1167 lines
37 KiB
Markdown

---
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**