""" MetaTrader 4 Direct Server Connection Client OrbiQuant IA Trading Platform Provides direct connection to MT4 server without requiring MT4 terminal. Uses the MT4 Manager API protocol or alternative open protocols. Options for MT4 connection: 1. dwx-zeromq-connector: Uses ZeroMQ bridge with MT4 EA 2. mt4-server-api: Direct TCP connection using reverse-engineered protocol 3. metaapi.cloud: Cloud service for MT4/MT5 API access 4. ctrader-fix: FIX protocol for cTrader (alternative) """ import asyncio import struct import socket import hashlib from datetime import datetime, timedelta from typing import Optional, List, Dict, Any, Callable from dataclasses import dataclass, field from enum import Enum, IntEnum import logging logger = logging.getLogger(__name__) class OrderType(IntEnum): """MT4 Order Types""" OP_BUY = 0 OP_SELL = 1 OP_BUYLIMIT = 2 OP_SELLLIMIT = 3 OP_BUYSTOP = 4 OP_SELLSTOP = 5 class TradeOperation(IntEnum): """MT4 Trade Operations""" OPEN = 1 CLOSE = 2 MODIFY = 3 DELETE = 4 @dataclass class MT4Tick: """Real-time tick data.""" symbol: str timestamp: datetime bid: float ask: float spread: float = field(init=False) def __post_init__(self): self.spread = self.ask - self.bid @dataclass class MT4Order: """MT4 Order information.""" ticket: int symbol: str order_type: OrderType lots: float open_price: float sl: float tp: float open_time: datetime close_price: Optional[float] = None close_time: Optional[datetime] = None profit: float = 0.0 swap: float = 0.0 commission: float = 0.0 magic: int = 0 comment: str = "" @dataclass class MT4AccountInfo: """MT4 Account information.""" login: int name: str server: str currency: str leverage: int balance: float equity: float margin: float free_margin: float margin_level: float profit: float @dataclass class MT4Symbol: """MT4 Symbol specification.""" symbol: str digits: int point: float spread: int stops_level: int contract_size: float tick_value: float tick_size: float min_lot: float max_lot: float lot_step: float swap_long: float swap_short: float class MT4ConnectionError(Exception): """MT4 Connection error.""" pass class MT4TradeError(Exception): """MT4 Trade execution error.""" pass class MT4Client: """ MetaTrader 4 Client using direct server connection. This implementation uses a hybrid approach: 1. For real-time data: WebSocket/TCP connection to a bridge service 2. For trading: REST API through MetaAPI or similar service For full direct connection without any bridge, you would need: - Reverse-engineered MT4 protocol (complex, may violate ToS) - Or: MT4 Manager API license from MetaQuotes (expensive) """ def __init__( self, server: str, login: int, password: str, investor_mode: bool = False, timeout: int = 30 ): self.server = server self.login = login self.password = password self.investor_mode = investor_mode self.timeout = timeout self._connected = False self._socket: Optional[socket.socket] = None self._account_info: Optional[MT4AccountInfo] = None self._symbols: Dict[str, MT4Symbol] = {} # Callbacks for real-time events self._tick_callbacks: List[Callable[[MT4Tick], None]] = [] self._order_callbacks: List[Callable[[MT4Order], None]] = [] @property def is_connected(self) -> bool: return self._connected async def connect(self) -> bool: """ Connect to MT4 server. Note: Direct MT4 server connection requires proprietary protocol. This implementation assumes a bridge service or MetaAPI. """ try: # Parse server address if ":" in self.server: host, port = self.server.rsplit(":", 1) port = int(port) else: host = self.server port = 443 logger.info(f"Connecting to MT4 server {host}:{port}") # For direct connection, we would establish TCP socket here # However, MT4 uses proprietary encryption # Instead, we'll use a bridge pattern self._connected = True logger.info(f"Connected to MT4 as {self.login}") # Load account info await self._load_account_info() return True except Exception as e: logger.error(f"Failed to connect to MT4: {e}") self._connected = False raise MT4ConnectionError(f"Connection failed: {e}") async def disconnect(self): """Disconnect from MT4 server.""" if self._socket: self._socket.close() self._socket = None self._connected = False logger.info("Disconnected from MT4") async def _load_account_info(self): """Load account information.""" # In real implementation, this would query the server # Placeholder for now pass async def get_account_info(self) -> MT4AccountInfo: """Get current account information.""" if not self._connected: raise MT4ConnectionError("Not connected") # Would query server for live data return self._account_info async def get_symbol_info(self, symbol: str) -> Optional[MT4Symbol]: """Get symbol specification.""" if symbol in self._symbols: return self._symbols[symbol] # Query server for symbol info # Placeholder - would parse server response return None async def get_tick(self, symbol: str) -> Optional[MT4Tick]: """Get current tick for symbol.""" if not self._connected: raise MT4ConnectionError("Not connected") # Query current prices # In real implementation, this uses the MarketInfo command return None async def subscribe_ticks( self, symbols: List[str], callback: Callable[[MT4Tick], None] ): """Subscribe to real-time tick updates.""" self._tick_callbacks.append(callback) # In real implementation, send subscription request to server logger.info(f"Subscribed to ticks for {symbols}") async def get_spread(self, symbol: str) -> float: """Get current spread for symbol in points.""" tick = await self.get_tick(symbol) if tick: symbol_info = await self.get_symbol_info(symbol) if symbol_info: return (tick.ask - tick.bid) / symbol_info.point return 0.0 async def get_spread_in_price(self, symbol: str) -> float: """Get current spread as price difference.""" tick = await self.get_tick(symbol) return tick.spread if tick else 0.0 # ========================================== # Trading Operations # ========================================== async def open_order( self, symbol: str, order_type: OrderType, lots: float, price: float = 0, sl: float = 0, tp: float = 0, slippage: int = 3, magic: int = 0, comment: str = "" ) -> Optional[int]: """ Open a new order. Args: symbol: Trading symbol order_type: Type of order (buy, sell, etc.) lots: Trade volume price: Order price (0 for market orders) sl: Stop loss price tp: Take profit price slippage: Maximum slippage in points magic: Expert Advisor magic number comment: Order comment Returns: Order ticket number or None if failed """ if not self._connected: raise MT4ConnectionError("Not connected") if self.investor_mode: raise MT4TradeError("Cannot trade in investor mode") logger.info(f"Opening {order_type.name} order for {symbol}, {lots} lots") # Build trade request # In real implementation, this sends TradeTransaction command # Placeholder return return None async def close_order( self, ticket: int, lots: Optional[float] = None, price: float = 0, slippage: int = 3 ) -> bool: """ Close an existing order. Args: ticket: Order ticket number lots: Volume to close (None = close all) price: Close price (0 for market) slippage: Maximum slippage Returns: True if successful """ if not self._connected: raise MT4ConnectionError("Not connected") logger.info(f"Closing order {ticket}") # Build close request # In real implementation, sends TradeTransaction close command return False async def modify_order( self, ticket: int, price: Optional[float] = None, sl: Optional[float] = None, tp: Optional[float] = None ) -> bool: """ Modify an existing order. Args: ticket: Order ticket price: New order price (for pending orders) sl: New stop loss tp: New take profit Returns: True if successful """ if not self._connected: raise MT4ConnectionError("Not connected") logger.info(f"Modifying order {ticket}") return False async def get_orders(self, symbol: Optional[str] = None) -> List[MT4Order]: """Get all open orders.""" if not self._connected: raise MT4ConnectionError("Not connected") # Query open orders from server return [] async def get_history( self, start_time: datetime, end_time: datetime, symbol: Optional[str] = None ) -> List[MT4Order]: """Get order history.""" if not self._connected: raise MT4ConnectionError("Not connected") # Query history from server return [] class MetaAPIClient(MT4Client): """ MT4/MT5 Client using MetaAPI.cloud service. MetaAPI provides REST/WebSocket API for MT4/MT5 without requiring the terminal or proprietary protocols. Requires MetaAPI account and token. """ METAAPI_URL = "https://mt-client-api-v1.agiliumtrade.agiliumtrade.ai" def __init__( self, token: str, account_id: str, server: str = "", login: int = 0, password: str = "", **kwargs ): super().__init__(server, login, password, **kwargs) self.token = token self.account_id = account_id self._session = None async def connect(self) -> bool: """Connect via MetaAPI.""" import aiohttp self._session = aiohttp.ClientSession( headers={"auth-token": self.token} ) try: # Deploy account if needed async with self._session.get( f"{self.METAAPI_URL}/users/current/accounts/{self.account_id}" ) as resp: if resp.status == 200: data = await resp.json() self._connected = True logger.info(f"Connected to MetaAPI account {self.account_id}") return True else: raise MT4ConnectionError(f"MetaAPI error: {resp.status}") except Exception as e: logger.error(f"MetaAPI connection failed: {e}") raise MT4ConnectionError(str(e)) async def disconnect(self): """Disconnect from MetaAPI.""" if self._session: await self._session.close() self._connected = False async def get_tick(self, symbol: str) -> Optional[MT4Tick]: """Get current tick via MetaAPI.""" if not self._session: return None try: async with self._session.get( f"{self.METAAPI_URL}/users/current/accounts/{self.account_id}/symbols/{symbol}/current-price" ) as resp: if resp.status == 200: data = await resp.json() return MT4Tick( symbol=symbol, timestamp=datetime.fromisoformat(data["time"].replace("Z", "+00:00")), bid=data["bid"], ask=data["ask"] ) except Exception as e: logger.error(f"Error getting tick: {e}") return None async def open_order( self, symbol: str, order_type: OrderType, lots: float, price: float = 0, sl: float = 0, tp: float = 0, slippage: int = 3, magic: int = 0, comment: str = "" ) -> Optional[int]: """Open order via MetaAPI.""" if not self._session: raise MT4ConnectionError("Not connected") action_type = "ORDER_TYPE_BUY" if order_type == OrderType.OP_BUY else "ORDER_TYPE_SELL" payload = { "symbol": symbol, "actionType": action_type, "volume": lots, "stopLoss": sl if sl > 0 else None, "takeProfit": tp if tp > 0 else None, "comment": comment } try: async with self._session.post( f"{self.METAAPI_URL}/users/current/accounts/{self.account_id}/trade", json=payload ) as resp: if resp.status == 200: data = await resp.json() return data.get("orderId") else: error = await resp.text() raise MT4TradeError(f"Trade failed: {error}") except Exception as e: logger.error(f"Order execution error: {e}") raise MT4TradeError(str(e)) class SpreadTracker: """ Tracks and analyzes spreads for trading cost calculations. """ def __init__(self, db_pool): self.db = db_pool self._spread_cache: Dict[str, Dict[str, float]] = {} async def record_spread( self, account_id: int, ticker_id: int, bid: float, ask: float, timestamp: datetime ): """Record a spread observation.""" spread_points = ask - bid spread_pct = (spread_points / ((bid + ask) / 2)) * 100 if bid > 0 else 0 async with self.db.acquire() as conn: await conn.execute( """ INSERT INTO broker_integration.broker_prices (account_id, ticker_id, timestamp, bid, ask, spread_points, spread_pct) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (account_id, ticker_id, timestamp) DO NOTHING """, account_id, ticker_id, timestamp, bid, ask, spread_points, spread_pct ) async def calculate_spread_statistics( self, account_id: int, ticker_id: int, period_hours: int = 24 ) -> Dict[str, float]: """Calculate spread statistics for a period.""" async with self.db.acquire() as conn: row = await conn.fetchrow( """ SELECT AVG(spread_points) as avg_spread, MIN(spread_points) as min_spread, MAX(spread_points) as max_spread, STDDEV(spread_points) as std_spread, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY spread_points) as median_spread, PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY spread_points) as p95_spread, COUNT(*) as sample_count FROM broker_integration.broker_prices WHERE account_id = $1 AND ticker_id = $2 AND timestamp > NOW() - INTERVAL '%s hours' """ % period_hours, account_id, ticker_id ) return dict(row) if row else {} async def get_session_spread( self, ticker_id: int, session: str = "london" ) -> float: """Get average spread for a trading session.""" async with self.db.acquire() as conn: row = await conn.fetchrow( """ SELECT avg_spread FROM broker_integration.spread_statistics WHERE ticker_id = $1 AND session_type = $2 ORDER BY period_start DESC LIMIT 1 """, ticker_id, session ) return row["avg_spread"] if row else 0.0 def calculate_spread_adjusted_rr( self, entry_price: float, stop_loss: float, take_profit: float, spread: float, is_long: bool ) -> Dict[str, float]: """ Calculate spread-adjusted risk/reward ratio. For LONG trades: - Entry is at ASK (higher), exit at BID (lower) - Effective entry = entry_price + spread/2 - Risk increases, reward decreases For SHORT trades: - Entry is at BID (lower), exit at ASK (higher) - Same adjustment applies """ if is_long: effective_entry = entry_price + spread / 2 risk = effective_entry - stop_loss reward = take_profit - effective_entry else: effective_entry = entry_price - spread / 2 risk = stop_loss - effective_entry reward = effective_entry - take_profit gross_rr = abs(take_profit - entry_price) / abs(entry_price - stop_loss) net_rr = reward / risk if risk > 0 else 0 spread_cost_pct = (spread / entry_price) * 100 return { "gross_rr": gross_rr, "net_rr": net_rr, "effective_entry": effective_entry, "adjusted_risk": risk, "adjusted_reward": reward, "spread_cost_pct": spread_cost_pct, "rr_reduction": gross_rr - net_rr }