Skip to main content
Glama
emerzon

MetaTrader5 MCP Server

by emerzon
trading.py50.8 kB
"""Trading functions for MetaTrader integration.""" import math from datetime import datetime, timezone, timedelta from typing import Optional, Tuple, Union, List, Dict, Any from .server import mcp from ..utils.mt5 import _auto_connect_wrapper from .config import mt5_config ExpirationValue = Union[int, float, str, datetime] _GTC_EXPIRATION_TOKENS = {"GTC", "GOOD_TILL_CANCEL", "GOOD_TILL_CANCELLED", "NONE", "NO_EXPIRATION"} def _to_server_time_naive(dt: datetime) -> datetime: """Convert a datetime (naive or aware) to broker/server local time and drop tzinfo. - If client/server tzs are configured (pytz), assume naive input is client tz, convert to server tz, and return naive server time. - Else, use MT5_TIME_OFFSET_MINUTES relative to UTC as a fallback. - If no hints, assume input is UTC. """ try: server_tz = mt5_config.get_server_tz() client_tz = mt5_config.get_client_tz() except Exception: server_tz = None client_tz = None aware = dt try: if dt.tzinfo is None: # Assume client-local when configured; otherwise assume UTC if client_tz is not None: aware = client_tz.localize(dt) if hasattr(client_tz, 'localize') else dt.replace(tzinfo=client_tz) else: aware = dt.replace(tzinfo=timezone.utc) except Exception: aware = dt.replace(tzinfo=timezone.utc) if server_tz is not None: try: server_aware = aware.astimezone(server_tz) return server_aware.replace(tzinfo=None) except Exception: pass # Fallback: offset seconds from UTC try: offset_sec = int(mt5_config.get_time_offset_seconds()) except Exception: offset_sec = 0 try: utc_dt = aware.astimezone(timezone.utc) except Exception: utc_dt = aware if aware.tzinfo is not None else aware.replace(tzinfo=timezone.utc) server_dt = utc_dt + timedelta(seconds=offset_sec) return server_dt.replace(tzinfo=None) def _normalize_pending_expiration(expiration: Optional[ExpirationValue]) -> Tuple[Optional[datetime], bool]: """Convert user-supplied expiration data into MetaTrader-friendly datetime objects. Returns a tuple ``(normalized_expiration, was_explicitly_provided)``. When ``was_explicitly_provided`` is False, callers should preserve the broker's existing order setting. When it is True and the normalized expiration is None, callers should submit a Good-Till-Cancelled order to clear any previous expiration. """ if expiration is None: return None, False if isinstance(expiration, datetime): return _to_server_time_naive(expiration), True if isinstance(expiration, (int, float)): if not math.isfinite(expiration) or expiration <= 0: return None, True try: # Treat numeric as epoch seconds in UTC, then convert to server time return _to_server_time_naive(datetime.fromtimestamp(expiration, tz=timezone.utc)), True except (OverflowError, OSError) as exc: raise ValueError(f"Expiration timestamp out of range: {expiration}") from exc if isinstance(expiration, str): cleaned = expiration.strip().strip('"').strip("'") if cleaned == "": return None, False upper_cleaned = cleaned.upper() if upper_cleaned in _GTC_EXPIRATION_TOKENS: return None, True # Regex for simple relative time like 'in 30 minutes', '1h', '30m' import re # Pattern for "in X units" or just "X units" or "Xunits" # Matches: "in 30 min", "30m", "1 hour", "2h", "10 seconds" simple_rel_pattern = re.compile(r'^(?:in\s+)?(\d+(?:\.\d+)?)\s*([a-zA-Z]+)$', re.IGNORECASE) match = simple_rel_pattern.match(cleaned) if match: val = float(match.group(1)) unit = match.group(2).lower() delta = None if unit in ('s', 'sec', 'secs', 'second', 'seconds'): delta = timedelta(seconds=val) elif unit in ('m', 'min', 'mins', 'minute', 'minutes'): delta = timedelta(minutes=val) elif unit in ('h', 'hr', 'hrs', 'hour', 'hours'): delta = timedelta(hours=val) elif unit in ('d', 'day', 'days'): delta = timedelta(days=val) elif unit in ('w', 'wk', 'weeks'): delta = timedelta(weeks=val) if delta is not None: # Relative to now (UTC) then converted to server time return _to_server_time_naive(datetime.now(timezone.utc) + delta), True # Try flexible date parsing first (e.g., 'tomorrow 14:00', 'in 2 hours') try: import dateparser # type: ignore dt = dateparser.parse(cleaned, settings={ 'RETURN_AS_TIMEZONE_AWARE': False, 'PREFER_DATES_FROM': 'future', 'RELATIVE_BASE': datetime.now(), # explicit base }) if dt is not None: return _to_server_time_naive(dt), True except Exception: pass # Fallbacks: numeric epoch or ISO8601 try: numeric = float(cleaned) if not math.isfinite(numeric) or numeric <= 0: return None, True try: return _to_server_time_naive(datetime.fromtimestamp(numeric, tz=timezone.utc)), True except (OverflowError, OSError) as exc: raise ValueError(f"Expiration timestamp out of range: {expiration}") from exc except ValueError: try: return _to_server_time_naive(datetime.fromisoformat(cleaned)), True except ValueError as exc: raise ValueError(f"Unsupported expiration format: {expiration}") from exc raise TypeError(f"Unsupported expiration type: {type(expiration).__name__}") @mcp.tool() def trading_account_info() -> dict: """Get account information (balance, equity, profit, margin level, free margin, account type, leverage, currency).""" import MetaTrader5 as mt5 @_auto_connect_wrapper def _get_account_info(): info = mt5.account_info() if info is None: return {"error": "Failed to get account info"} return { "balance": info.balance, "equity": info.equity, "profit": info.profit, "margin": info.margin, "margin_free": info.margin_free, "margin_level": info.margin_level, "currency": info.currency, "leverage": info.leverage, "trade_allowed": info.trade_allowed, "trade_expert": info.trade_expert, } return _get_account_info() @mcp.tool() def trading_deals_history(from_date: Optional[str] = None, to_date: Optional[str] = None, symbol: Optional[str] = None) -> List[Dict[str, Any]]: """Get historical deals as tabular data. Date input in format: 'YYYY-MM-DD'.""" import MetaTrader5 as mt5 import pandas as pd from datetime import datetime @_auto_connect_wrapper def _get_deals(): try: if from_date: from_dt = datetime.strptime(from_date, '%Y-%m-%d') else: from_dt = datetime(2020, 1, 1) if to_date: to_dt = datetime.strptime(to_date, '%Y-%m-%d') else: to_dt = datetime.now() if symbol: deals = mt5.history_deals_get(from_dt, to_dt, symbol=symbol) else: deals = mt5.history_deals_get(from_dt, to_dt) if deals is None or len(deals) == 0: return {"message": "No deals found"} df = pd.DataFrame(list(deals), columns=deals[0]._asdict().keys()) df['time'] = pd.to_datetime(df['time'], unit='s') return df.to_dict(orient='records') except Exception as e: return {"error": str(e)} return _get_deals() @mcp.tool() def trading_orders_active(from_date: Optional[str] = None, to_date: Optional[str] = None, symbol: Optional[str] = None) -> List[Dict[str, Any]]: """Get historical orders as tabular data. Date input in format: 'YYYY-MM-DD'""" import MetaTrader5 as mt5 import pandas as pd from datetime import datetime @_auto_connect_wrapper def _get_orders(): try: if from_date: from_dt = datetime.strptime(from_date, '%Y-%m-%d') else: from_dt = datetime(2020, 1, 1) if to_date: to_dt = datetime.strptime(to_date, '%Y-%m-%d') else: to_dt = datetime.now() if symbol: orders = mt5.history_orders_get(from_dt, to_dt, symbol=symbol) else: orders = mt5.history_orders_get(from_dt, to_dt) if orders is None or len(orders) == 0: return {"message": "No orders found"} df = pd.DataFrame(list(orders), columns=orders[0]._asdict().keys()) df['time_setup'] = pd.to_datetime(df['time_setup'], unit='s') if 'time_done' in df.columns: df['time_done'] = pd.to_datetime(df['time_done'], unit='s') return df.to_dict(orient='records') except Exception as e: return {"error": str(e)} return _get_orders() @mcp.tool() def trading_positions_get( symbol: Optional[str] = None, ticket: Optional[Union[int, str]] = None ) -> List[Dict[str, Any]]: """Get open positions. Parameters: - symbol: Filter by symbol name. - ticket: Filter by position ticket ID. If neither is provided, returns all open positions. """ import MetaTrader5 as mt5 import pandas as pd @_auto_connect_wrapper def _get_positions(): try: if ticket is not None: t_int = int(ticket) positions = mt5.positions_get(ticket=t_int) if positions is None or len(positions) == 0: return [{"message": f"No position found with ID {ticket}"}] elif symbol is not None: positions = mt5.positions_get(symbol=symbol) if positions is None or len(positions) == 0: return [{"message": f"No open positions for {symbol}"}] else: positions = mt5.positions_get() if positions is None or len(positions) == 0: return [{"message": "No open positions"}] df = pd.DataFrame(list(positions), columns=positions[0]._asdict().keys()) if 'time' in df.columns: df['time'] = pd.to_datetime(df['time'], unit='s') return df.to_dict(orient='records') except Exception as e: return [{"error": str(e)}] return _get_positions() @mcp.tool() def trading_pending_get( symbol: Optional[str] = None, ticket: Optional[Union[int, str]] = None ) -> List[Dict[str, Any]]: """Get pending orders. Parameters: - symbol: Filter by symbol name. - ticket: Filter by order ticket ID. If neither is provided, returns all pending orders. """ import MetaTrader5 as mt5 import pandas as pd @_auto_connect_wrapper def _get_pending(): try: if ticket is not None: t_int = int(ticket) orders = mt5.orders_get(ticket=t_int) if orders is None or len(orders) == 0: return [{"message": f"No pending order found with ID {ticket}"}] elif symbol is not None: orders = mt5.orders_get(symbol=symbol) if orders is None or len(orders) == 0: return [{"message": f"No pending orders for {symbol}"}] else: orders = mt5.orders_get() if orders is None or len(orders) == 0: return [{"message": "No pending orders"}] df = pd.DataFrame(list(orders), columns=orders[0]._asdict().keys()) if 'time_setup' in df.columns: df['time_setup'] = pd.to_datetime(df['time_setup'], unit='s') return df.to_dict(orient='records') except Exception as e: return [{"error": str(e)}] return _get_pending() @mcp.tool() def trading_orders_place_market( symbol: str, volume: float, type: str, stop_loss: Optional[Union[int, float]] = None, take_profit: Optional[Union[int, float]] = None, ) -> dict: """ Place a market order. Parameters: symbol: Symbol name (e.g., 'EURUSD') volume: Lot size. (e.g. 1.5) type: Order type ('BUY' or 'SELL') stop_loss (optional): Stop loss price. take_profit (optional): Take profit price. """ import MetaTrader5 as mt5 @_auto_connect_wrapper def _place_market_order(): try: symbol_info = mt5.symbol_info(symbol) if symbol_info is None: return {"error": f"Symbol {symbol} not found"} if not symbol_info.visible: if not mt5.symbol_select(symbol, True): return {"error": f"Failed to select symbol {symbol}"} current_tick = mt5.symbol_info_tick(symbol) if current_tick is None: return {"error": f"Failed to get current price for {symbol}"} price = current_tick.ask if type.upper() == "BUY" else current_tick.bid # Price normalization helper point = float(symbol_info.point or 0.0) if hasattr(symbol_info, "point") else 0.0 digits = int(symbol_info.digits) if hasattr(symbol_info, "digits") else 5 def _normalize_price(val: Optional[Union[int, float]]) -> Optional[float]: try: if val is None: return None v = float(val) if not math.isfinite(v): return None if point and point > 0: # Align to symbol precision v = round(v / point) * point else: v = round(v, digits) return v except Exception: return None norm_sl = _normalize_price(stop_loss) if stop_loss not in (None, 0) else None norm_tp = _normalize_price(take_profit) if take_profit not in (None, 0) else None # SL/TP validation for market orders if norm_sl is not None: if type.upper() == "BUY" and norm_sl >= price: return {"error": f"stop_loss must be below entry for BUY orders. sl={norm_sl}, price={price}"} if type.upper() == "SELL" and norm_sl <= price: return {"error": f"stop_loss must be above entry for SELL orders. sl={norm_sl}, price={price}"} if norm_tp is not None: if type.upper() == "BUY" and norm_tp <= price: return {"error": f"take_profit must be above entry for BUY orders. tp={norm_tp}, price={price}"} if type.upper() == "SELL" and norm_tp >= price: return {"error": f"take_profit must be below entry for SELL orders. tp={norm_tp}, price={price}"} # Place market order without TP/SL first (TRADE_ACTION_DEAL doesn't reliably support them) request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, "volume": volume, "type": mt5.ORDER_TYPE_BUY if type.upper() == "BUY" else mt5.ORDER_TYPE_SELL, "price": price, "deviation": 20, "magic": 234000, "comment": "MCP order", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC, } result = mt5.order_send(request) if result is None: # Surface MetaTrader last_error when available for easier debugging try: err = mt5.last_error() except Exception: err = None return {"error": "Failed to send order", "last_error": err} if getattr(result, "retcode", None) != mt5.TRADE_RETCODE_DONE: return { "error": "Failed to send order", "retcode": result.retcode, "comment": result.comment, "request_id": result.request_id, "request": request, "last_error": mt5.last_error() if hasattr(mt5, "last_error") else None, } # If TP/SL were specified, modify the position immediately position_ticket = result.order sl_tp_modified = False sl_tp_error = None if norm_sl is not None or norm_tp is not None: try: # Get the position that was just opened positions = mt5.positions_get(ticket=position_ticket) if positions and len(positions) > 0: # Use TRADE_ACTION_SLTP to set TP/SL on the position modify_request = { "action": mt5.TRADE_ACTION_SLTP, "position": position_ticket, "sl": norm_sl or 0.0, "tp": norm_tp or 0.0, "magic": 234000, "comment": "MCP order - set TP/SL", } modify_result = mt5.order_send(modify_request) if modify_result and getattr(modify_result, "retcode", None) == mt5.TRADE_RETCODE_DONE: sl_tp_modified = True else: sl_tp_error = "Failed to set TP/SL" else: sl_tp_error = "Position not found for TP/SL modification" except Exception as e: sl_tp_error = f"Error setting TP/SL: {str(e)}" return { "retcode": result.retcode, "deal": result.deal, "order": result.order, "volume": result.volume, "price": result.price, "bid": result.bid, "ask": result.ask, "comment": result.comment, "request_id": result.request_id, "sl": norm_sl, "tp": norm_tp, "sl_tp_modified": sl_tp_modified, "sl_tp_error": sl_tp_error, } except Exception as e: return {"error": str(e)} return _place_market_order() @mcp.tool() def trading_pending_place( symbol: str, volume: float, type: str, price: float, stop_loss: Optional[Union[int, float]] = 0, take_profit: Optional[Union[int, float]] = 0, expiration: Optional[ExpirationValue] = None, ) -> dict: """ Place a pending order. Parameters: symbol: Symbol name (e.g., 'EURUSD') volume: Lot size. (e.g. 1.5) type: Order type ('BUY', 'SELL'). price: Pending order price. stop_loss (optional): Stop loss price. take_profit (optional): Take profit price. expiration (optional): Accepts GTC tokens (GTC/GOOD_TILL_CANCEL/...), ISO datetime, numeric epoch seconds, or natural language via dateparser (e.g., 'tomorrow 14:00', 'in 2 hours'). Use 0 or 'GTC' to submit GTC orders. """ import MetaTrader5 as mt5 @_auto_connect_wrapper def _place_pending_order(): try: symbol_info = mt5.symbol_info(symbol) if symbol_info is None: return {"error": f"Symbol {symbol} not found"} if not symbol_info.visible: if not mt5.symbol_select(symbol, True): return {"error": f"Failed to select symbol {symbol}"} current_price = mt5.symbol_info_tick(symbol) if current_price is None: return {"error": f"Failed to get current price for {symbol}"} # Normalize and validate requested order type t = (type or "").strip().upper() explicit_map = { "BUYLIMIT": mt5.ORDER_TYPE_BUY_LIMIT, "BUY_LIMIT": mt5.ORDER_TYPE_BUY_LIMIT, "BUYSTOP": mt5.ORDER_TYPE_BUY_STOP, "BUY_STOP": mt5.ORDER_TYPE_BUY_STOP, "SELLLIMIT": mt5.ORDER_TYPE_SELL_LIMIT, "SELL_LIMIT": mt5.ORDER_TYPE_SELL_LIMIT, "SELLSTOP": mt5.ORDER_TYPE_SELL_STOP, "SELL_STOP": mt5.ORDER_TYPE_SELL_STOP, } order_type = None if t in explicit_map: order_type = explicit_map[t] elif t in ("BUY", "LONG"): order_type = mt5.ORDER_TYPE_BUY_LIMIT if price < current_price.ask else mt5.ORDER_TYPE_BUY_STOP elif t in ("SELL", "SHORT"): order_type = mt5.ORDER_TYPE_SELL_LIMIT if price > current_price.bid else mt5.ORDER_TYPE_SELL_STOP else: return {"error": f"Unsupported type '{type}'. Use one of: BUY, SELL, BUYLIMIT, BUYSTOP, SELLLIMIT, SELLSTOP."} # Basic side/price sanity checks for explicit pending types bid = float(getattr(current_price, "bid", 0.0) or 0.0) ask = float(getattr(current_price, "ask", 0.0) or 0.0) point = float(symbol_info.point or 0.0) if hasattr(symbol_info, "point") else 0.0 digits = int(symbol_info.digits) if hasattr(symbol_info, "digits") else 5 def _normalize_price(val: Optional[Union[int, float]]) -> Optional[float]: try: if val is None: return None v = float(val) if not math.isfinite(v): return None if point and point > 0: # Align to symbol precision v = round(v / point) * point else: v = round(v, digits) return v except Exception: return None norm_price = _normalize_price(price) norm_sl = _normalize_price(stop_loss) if stop_loss not in (None, 0) else None norm_tp = _normalize_price(take_profit) if take_profit not in (None, 0) else None if order_type == mt5.ORDER_TYPE_BUY_LIMIT and not (price < ask): return {"error": f"Price must be below ask for BUYLIMIT. price={price}, ask={ask}"} if order_type == mt5.ORDER_TYPE_BUY_STOP and not (price > ask): return {"error": f"Price must be above ask for BUYSTOP. price={price}, ask={ask}"} if order_type == mt5.ORDER_TYPE_SELL_LIMIT and not (price > bid): return {"error": f"Price must be above bid for SELLLIMIT. price={price}, bid={bid}"} if order_type == mt5.ORDER_TYPE_SELL_STOP and not (price < bid): return {"error": f"Price must be below bid for SELLSTOP. price={price}, bid={bid}"} normalized_expiration, expiration_specified = _normalize_pending_expiration(expiration) # SL/TP sanity relative to entry if norm_sl is not None: if order_type in (mt5.ORDER_TYPE_BUY_LIMIT, mt5.ORDER_TYPE_BUY_STOP) and norm_sl >= norm_price: return {"error": f"stop_loss must be below entry for BUY orders. sl={norm_sl}, price={norm_price}"} if order_type in (mt5.ORDER_TYPE_SELL_LIMIT, mt5.ORDER_TYPE_SELL_STOP) and norm_sl <= norm_price: return {"error": f"stop_loss must be above entry for SELL orders. sl={norm_sl}, price={norm_price}"} if norm_tp is not None: if order_type in (mt5.ORDER_TYPE_BUY_LIMIT, mt5.ORDER_TYPE_BUY_STOP) and norm_tp <= norm_price: return {"error": f"take_profit must be above entry for BUY orders. tp={norm_tp}, price={norm_price}"} if order_type in (mt5.ORDER_TYPE_SELL_LIMIT, mt5.ORDER_TYPE_SELL_STOP) and norm_tp >= norm_price: return {"error": f"take_profit must be below entry for SELL orders. tp={norm_tp}, price={norm_price}"} request = { "action": mt5.TRADE_ACTION_PENDING, "symbol": symbol, "volume": volume, "type": order_type, "price": norm_price, "sl": norm_sl or 0.0, "tp": norm_tp or 0.0, "deviation": 20, "magic": 234000, "comment": "MCP pending order", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC, } if expiration_specified: if normalized_expiration is None: request["type_time"] = mt5.ORDER_TIME_GTC else: request["type_time"] = mt5.ORDER_TIME_SPECIFIED request["expiration"] = normalized_expiration result = mt5.order_send(request) if result is None: # Surface MetaTrader last_error when available for easier debugging try: err = mt5.last_error() except Exception: err = None return {"error": "Failed to send pending order", "last_error": err, "request": request} if getattr(result, "retcode", None) != mt5.TRADE_RETCODE_DONE: return { "error": "Failed to send pending order", "retcode": result.retcode, "comment": result.comment, "request_id": result.request_id, "request": request, "last_error": mt5.last_error() if hasattr(mt5, "last_error") else None, } return { "success": True, "retcode": result.retcode, "deal": result.deal, "order": result.order, "volume": result.volume, "price": result.price, "bid": result.bid, "ask": result.ask, "comment": result.comment, "request_id": result.request_id, } except Exception as e: return {"error": str(e)} return _place_pending_order() @mcp.tool() def trading_positions_modify(id: Union[int, str], stop_loss: Optional[Union[int, float]] = None, take_profit: Optional[Union[int, float]] = None) -> dict: """Modify an open position by ID.""" import MetaTrader5 as mt5 @_auto_connect_wrapper def _modify_position(): try: ticket = int(id) positions = mt5.positions_get(ticket=ticket) if positions is None or len(positions) == 0: return {"error": f"Position {id} not found"} position = positions[0] # Get symbol info for price normalization symbol_info = mt5.symbol_info(position.symbol) if symbol_info is None: return {"error": f"Failed to get symbol info for {position.symbol}"} point = float(symbol_info.point or 0.0) if hasattr(symbol_info, "point") else 0.0 digits = int(symbol_info.digits) if hasattr(symbol_info, "digits") else 5 def _normalize_price(val: Optional[Union[int, float]]) -> Optional[float]: """Normalize price to symbol precision.""" try: if val is None or val == 0: return None v = float(val) if not math.isfinite(v): return None if point and point > 0: # Align to symbol precision v = round(v / point) * point else: v = round(v, digits) return v except Exception: return None # Normalize SL/TP values norm_sl = _normalize_price(stop_loss) if stop_loss is not None else (position.sl or 0.0) norm_tp = _normalize_price(take_profit) if take_profit is not None else (position.tp or 0.0) # Ensure SL/TP values are 0.0 if they should be removed if norm_sl is None: norm_sl = 0.0 if norm_tp is None: norm_tp = 0.0 request = { "action": mt5.TRADE_ACTION_SLTP, "position": ticket, "sl": norm_sl, "tp": norm_tp, "magic": 234000, "comment": "MCP modify position", } result = mt5.order_send(request) if result is None: # surface the MT5 terminal error for debugging try: last_err = mt5.last_error() except Exception: last_err = None return {"error": "Failed to modify position", "request": request, "last_error": last_err} if getattr(result, "retcode", None) != mt5.TRADE_RETCODE_DONE: return { "error": "Failed to modify position", "retcode": result.retcode, "comment": result.comment, "request_id": result.request_id, "request": request, "last_error": mt5.last_error() if hasattr(mt5, "last_error") else None, } return { "success": True, "retcode": result.retcode, "deal": result.deal, "order": result.order, "comment": result.comment, "request_id": result.request_id, } except Exception as e: return {"error": str(e)} return _modify_position() @mcp.tool() def trading_pending_modify( id: Union[int, str], price: Optional[Union[int, float]] = None, stop_loss: Optional[Union[int, float]] = None, take_profit: Optional[Union[int, float]] = None, expiration: Optional[ExpirationValue] = None, ) -> dict: """Modify a pending order by ID.""" import MetaTrader5 as mt5 @_auto_connect_wrapper def _modify_pending_order(): try: ticket = int(id) orders = mt5.orders_get(ticket=ticket) if orders is None or len(orders) == 0: return {"error": f"Pending order {id} not found"} order = orders[0] normalized_expiration, expiration_specified = _normalize_pending_expiration(expiration) request = { "action": mt5.TRADE_ACTION_MODIFY, "order": ticket, "price": price if price is not None else order.price_open, "sl": stop_loss if stop_loss is not None else order.sl, "tp": take_profit if take_profit is not None else order.tp, "magic": 234000, "comment": "MCP modify pending order", } if expiration_specified: if normalized_expiration is None: request["type_time"] = mt5.ORDER_TIME_GTC else: request["type_time"] = mt5.ORDER_TIME_SPECIFIED request["expiration"] = normalized_expiration else: current_type_time = getattr(order, "type_time", None) current_expiration = getattr(order, "time_expiration", None) if current_type_time is not None: request["type_time"] = current_type_time if current_type_time == mt5.ORDER_TIME_SPECIFIED and current_expiration: request["expiration"] = current_expiration result = mt5.order_send(request) if result is None: return {"error": "Failed to modify pending order"} return { "retcode": result.retcode, "deal": result.deal, "order": result.order, "comment": result.comment, "request_id": result.request_id, } except Exception as e: return {"error": str(e)} return _modify_pending_order() @mcp.tool() def trading_positions_close( ticket: Optional[Union[int, str]] = None, symbol: Optional[str] = None, profit_only: bool = False, loss_only: bool = False, ) -> dict: """Close open positions. Parameters: - ticket: Close a specific position by ticket ID. - symbol: Close all positions for a specific symbol. - profit_only: If True, only close profitable positions. - loss_only: If True, only close losing positions. If no parameters are provided, closes ALL open positions. """ import MetaTrader5 as mt5 @_auto_connect_wrapper def _close_positions(): try: # 1. Fetch positions based on criteria if ticket is not None: t_int = int(ticket) positions = mt5.positions_get(ticket=t_int) if positions is None or len(positions) == 0: return {"error": f"Position {ticket} not found"} elif symbol is not None: positions = mt5.positions_get(symbol=symbol) if positions is None or len(positions) == 0: return {"message": f"No open positions for {symbol}"} else: positions = mt5.positions_get() if positions is None or len(positions) == 0: return {"message": "No open positions"} # 2. Filter positions to_close = [] for pos in positions: if profit_only and pos.profit <= 0: continue if loss_only and pos.profit >= 0: continue to_close.append(pos) if not to_close: return {"message": "No positions matched criteria"} # 3. Close positions results = [] for position in to_close: tick = mt5.symbol_info_tick(position.symbol) if tick is None: results.append({"ticket": position.ticket, "error": f"Failed to get tick data for {position.symbol}"}) continue close_price = tick.bid if position.type == mt5.ORDER_TYPE_BUY else tick.ask close_type = mt5.ORDER_TYPE_SELL if position.type == mt5.ORDER_TYPE_BUY else mt5.ORDER_TYPE_BUY request = { "action": mt5.TRADE_ACTION_DEAL, "position": position.ticket, "symbol": position.symbol, "volume": position.volume, "type": close_type, "price": close_price, "deviation": 20, "magic": 234000, "comment": "MCP close position", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC, } result = mt5.order_send(request) if result is None: results.append({"ticket": position.ticket, "error": "Failed to send close order"}) else: res_dict = { "ticket": position.ticket, "retcode": result.retcode, "deal": result.deal, "order": result.order, "volume": result.volume, "price": result.price, "comment": result.comment, } results.append(res_dict) # If only one position was targeted by ticket, return single result for backward compatibility/convenience if ticket is not None and len(results) == 1: return results[0] return {"closed_count": len(results), "results": results} except Exception as e: return {"error": str(e)} return _close_positions() @mcp.tool() def trading_pending_cancel( ticket: Optional[Union[int, str]] = None, symbol: Optional[str] = None ) -> dict: """Cancel pending orders. Parameters: - ticket: Cancel a specific order by ticket ID. - symbol: Cancel all pending orders for a specific symbol. If no parameters are provided, cancels ALL pending orders. """ import MetaTrader5 as mt5 @_auto_connect_wrapper def _cancel_pending(): try: # 1. Fetch orders based on criteria if ticket is not None: t_int = int(ticket) orders = mt5.orders_get(ticket=t_int) if orders is None or len(orders) == 0: return {"error": f"Pending order {ticket} not found"} elif symbol is not None: orders = mt5.orders_get(symbol=symbol) if orders is None or len(orders) == 0: return {"message": f"No pending orders for {symbol}"} else: orders = mt5.orders_get() if orders is None or len(orders) == 0: return {"message": "No pending orders"} # 2. Cancel orders results = [] for order in orders: request = { "action": mt5.TRADE_ACTION_REMOVE, "order": order.ticket, "magic": 234000, "comment": "MCP cancel pending order", } result = mt5.order_send(request) if result is None: results.append({"ticket": order.ticket, "error": "Failed to send cancel order"}) else: results.append({ "ticket": order.ticket, "retcode": result.retcode, "deal": result.deal, "order": result.order, "comment": result.comment, }) # If only one order was targeted by ticket, return single result if ticket is not None and len(results) == 1: return results[0] return {"cancelled_count": len(results), "results": results} except Exception as e: return {"error": str(e)} return _cancel_pending() @mcp.tool() def trading_risk_analyze( symbol: Optional[str] = None, desired_risk_pct: Optional[float] = None, proposed_entry: Optional[float] = None, proposed_sl: Optional[float] = None, proposed_tp: Optional[float] = None, ) -> dict: """Analyze risk exposure for existing positions and calculate position sizing for new trades. Use Cases: ---------- 1. Analyze current portfolio risk 2. Calculate proper position size for a new trade based on risk % 3. Get R:R ratios for existing positions Parameters: ----------- symbol : str, optional Analyze positions for a specific symbol. If not provided, analyzes all positions. desired_risk_pct : float, optional Desired risk percentage for position sizing (e.g., 2.0 for 2% risk per trade) **Required with proposed_entry and proposed_sl for position sizing** proposed_entry : float, optional Entry price for new trade. Used with desired_risk_pct to calculate lot size. proposed_sl : float, optional Stop loss price for new trade. proposed_tp : float, optional Take profit price for new trade. Used to calculate R:R ratio. Returns: -------- dict - success: bool - account: {equity, currency} - portfolio_risk: {total_risk_currency, total_risk_pct, positions_count} - positions: list of position risk details - position_sizing: (if desired_risk_pct provided) suggested volume and risk metrics Examples: --------- # Analyze current portfolio risk trading_risk_analyze() # Analyze risk for specific symbol trading_risk_analyze(symbol="EURUSD") # Calculate position size for 2% risk trading_risk_analyze( symbol="EURUSD", desired_risk_pct=2.0, proposed_entry=1.1000, proposed_sl=1.0950, proposed_tp=1.1100 ) """ import MetaTrader5 as mt5 @_auto_connect_wrapper def _analyze_risk(): try: # Get account info account = mt5.account_info() if account is None: return {"error": "Failed to get account info"} equity = float(account.equity) currency = account.currency # Get positions if symbol: positions = mt5.positions_get(symbol=symbol) else: positions = mt5.positions_get() if positions is None: positions = [] # Analyze each position position_risks = [] total_risk_currency = 0.0 positions_without_sl = 0 total_notional_exposure = 0.0 for pos in positions: try: # Get symbol info for calculations sym_info = mt5.symbol_info(pos.symbol) if sym_info is None: continue # Calculate risk per position entry_price = float(pos.price_open) sl_price = float(pos.sl) if pos.sl and pos.sl > 0 else None tp_price = float(pos.tp) if pos.tp and pos.tp > 0 else None volume = float(pos.volume) # Contract specifications contract_size = float(sym_info.trade_contract_size) point = float(sym_info.point) tick_value = float(sym_info.trade_tick_value) # Calculate notional exposure (position value) notional_value = volume * contract_size * entry_price / (contract_size if contract_size > 1 else 1) total_notional_exposure += notional_value risk_currency = None risk_pct = None reward_currency = None rr_ratio = None risk_status = "undefined" if sl_price: # Calculate risk in price distance if pos.type == 0: # BUY risk_points = (entry_price - sl_price) / point else: # SELL risk_points = (sl_price - entry_price) / point # Risk in account currency risk_currency = abs(risk_points * tick_value * volume) risk_pct = (risk_currency / equity) * 100.0 if equity > 0 else 0.0 total_risk_currency += risk_currency risk_status = "defined" # Calculate reward if TP is set if tp_price: if pos.type == 0: # BUY reward_points = (tp_price - entry_price) / point else: # SELL reward_points = (entry_price - tp_price) / point reward_currency = abs(reward_points * tick_value * volume) if risk_currency > 0: rr_ratio = reward_currency / risk_currency else: # No SL = unlimited risk positions_without_sl += 1 risk_status = "unlimited" position_risks.append({ "ticket": pos.ticket, "symbol": pos.symbol, "type": "BUY" if pos.type == 0 else "SELL", "volume": volume, "entry": entry_price, "sl": sl_price, "tp": tp_price, "risk_currency": round(risk_currency, 2) if risk_currency else None, "risk_pct": round(risk_pct, 2) if risk_pct else None, "risk_status": risk_status, "notional_value": round(notional_value, 2), "reward_currency": round(reward_currency, 2) if reward_currency else None, "rr_ratio": round(rr_ratio, 2) if rr_ratio else None, }) except Exception as e: continue # Calculate total portfolio risk total_risk_pct = (total_risk_currency / equity) * 100.0 if equity > 0 else 0.0 notional_exposure_pct = (total_notional_exposure / equity) * 100.0 if equity > 0 else 0.0 # Determine overall risk level overall_risk_status = "defined" if positions_without_sl > 0: overall_risk_status = "unlimited" elif total_risk_pct > 10: overall_risk_status = "high" elif total_risk_pct > 5: overall_risk_status = "moderate" else: overall_risk_status = "low" result = { "success": True, "account": { "equity": round(equity, 2), "currency": currency, }, "portfolio_risk": { "overall_risk_status": overall_risk_status, "total_risk_currency": round(total_risk_currency, 2), "total_risk_pct": round(total_risk_pct, 2), "positions_count": len(position_risks), "positions_without_sl": positions_without_sl, "notional_exposure": round(total_notional_exposure, 2), "notional_exposure_pct": round(notional_exposure_pct, 2), }, "positions": position_risks, } # Add warning if positions lack SL if positions_without_sl > 0: result["warning"] = f"{positions_without_sl} position(s) without stop loss - UNLIMITED RISK!" # Calculate position sizing if desired_risk_pct is provided if desired_risk_pct is not None and proposed_entry is not None and proposed_sl is not None: if not symbol: return {"error": "symbol is required for position sizing"} sym_info = mt5.symbol_info(symbol) if sym_info is None: return {"error": f"Symbol {symbol} not found"} # Calculate position size contract_size = float(sym_info.trade_contract_size) point = float(sym_info.point) tick_value = float(sym_info.trade_tick_value) min_volume = float(sym_info.volume_min) max_volume = float(sym_info.volume_max) volume_step = float(sym_info.volume_step) # Risk amount in account currency risk_amount = equity * (desired_risk_pct / 100.0) # SL distance in points sl_distance_points = abs(proposed_entry - proposed_sl) / point if sl_distance_points > 0: # Calculate volume suggested_volume = risk_amount / (sl_distance_points * tick_value) # Round to volume step suggested_volume = round(suggested_volume / volume_step) * volume_step # Clamp to min/max suggested_volume = max(min_volume, min(suggested_volume, max_volume)) # Calculate actual risk with suggested volume actual_risk = sl_distance_points * tick_value * suggested_volume actual_risk_pct = (actual_risk / equity) * 100.0 # Calculate R:R if TP is provided rr_ratio = None reward_currency = None if proposed_tp is not None: tp_distance_points = abs(proposed_tp - proposed_entry) / point reward_currency = tp_distance_points * tick_value * suggested_volume if actual_risk > 0: rr_ratio = reward_currency / actual_risk result["position_sizing"] = { "symbol": symbol, "suggested_volume": round(suggested_volume, 2), "entry": proposed_entry, "sl": proposed_sl, "tp": proposed_tp, "risk_currency": round(actual_risk, 2), "risk_pct": round(actual_risk_pct, 2), "reward_currency": round(reward_currency, 2) if reward_currency else None, "rr_ratio": round(rr_ratio, 2) if rr_ratio else None, } else: result["position_sizing_error"] = "SL distance must be greater than 0" return result except Exception as e: return {"error": str(e)} return _analyze_risk()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/emerzon/mt-data-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server