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>
1167 lines
37 KiB
Markdown
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**
|