trading-platform/docs/01-arquitectura/INTEGRACION-METATRADER4.md
rckrdmrd a7cca885f0 feat: Major platform documentation and architecture updates
Changes include:
- Updated architecture documentation
- Enhanced module definitions (OQI-001 to OQI-008)
- ML integration documentation updates
- Trading strategies documentation
- Orchestration and inventory updates
- Docker configuration updates

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:33:35 -06:00

37 KiB

id title type project version updated_date
INTEGRACION-METATRADER4 Integracion MetaTrader4 via MetaAPI Documentation trading-platform 1.0.0 2026-01-04

Integracion MetaTrader4 via MetaAPI

Version: 1.0.0 Fecha: 2025-12-08 Modulo: Trading Operations Autor: Trading Strategist - OrbiQuant IA


Tabla de Contenidos

  1. Vision General
  2. Arquitectura de Integracion
  3. MetaAPI Setup
  4. Account Management
  5. Trade Execution
  6. Price Adjustments
  7. API Endpoints
  8. Error Handling
  9. Implementacion
  10. 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                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌─────────────────────┐                                                    │
│  │   OrbiQuant IA      │                                                    │
│  │   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

# 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

# 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

# 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': 'OrbiQuantIA',
        'keywords': ['orbiquant', '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

# 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

# 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 'OrbiQuant IA',
                        '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 'OrbiQuant IA',
                        '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

# 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

# api/trading_api.py

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI(title="OrbiQuant 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

# 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
# docker-compose.trading.yaml

version: '3.8'

services:
  trading-service:
    build:
      context: .
      dockerfile: Dockerfile.trading
    container_name: orbiquant-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://orbiquant_user:orbiquant_dev_2025@postgres:5438/orbiquant_trading
    volumes:
      - ./config:/app/config
      - ./logs:/app/logs
    depends_on:
      - redis
      - postgres
    restart: unless-stopped

volumes:
  trading_logs:

Testing

Test Suite

# 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 - OrbiQuant IA