Refactor code for improved readability and consistency
- Cleaned up whitespace and formatting in various files including http.py, language.py, logger.py, safe_exec.py, and SQL migration scripts. - Consolidated import statements and removed unnecessary blank lines. - Updated logging configuration for better clarity. - Enhanced the safe execution code with improved error handling and logging. - Removed commented-out code and unnecessary variables in backfill_zero_trades.py and other scripts. - Added a pyproject.toml for Ruff and Vulture configuration. - Introduced requirements-dev.txt for development dependencies. - Removed commented-out stock entries in init.sql for cleaner migration scripts.
This commit is contained in:
@@ -5,14 +5,13 @@ Uses official MetaTrader5 Python library to connect to MT5 terminal for trading.
|
||||
Note: Requires Windows platform and MT5 terminal installed.
|
||||
"""
|
||||
|
||||
import time
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from app.services.mt5_trading.symbols import normalize_symbol
|
||||
from app.utils.logger import get_logger
|
||||
from app.services.mt5_trading.symbols import normalize_symbol, parse_symbol
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -26,6 +25,7 @@ def _ensure_mt5():
|
||||
if mt5 is None:
|
||||
try:
|
||||
import MetaTrader5 as _mt5
|
||||
|
||||
mt5 = _mt5
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
@@ -38,6 +38,7 @@ def _ensure_mt5():
|
||||
@dataclass
|
||||
class MT5Config:
|
||||
"""MT5 connection configuration."""
|
||||
|
||||
login: int = 0 # MT5 account number
|
||||
password: str = "" # MT5 password
|
||||
server: str = "" # Broker server name (e.g., "ICMarkets-Demo")
|
||||
@@ -49,6 +50,7 @@ class MT5Config:
|
||||
@dataclass
|
||||
class OrderResult:
|
||||
"""Order execution result."""
|
||||
|
||||
success: bool
|
||||
order_id: int = 0
|
||||
deal_id: int = 0
|
||||
@@ -62,7 +64,7 @@ class OrderResult:
|
||||
class MT5Client:
|
||||
"""
|
||||
MetaTrader 5 Trading Client
|
||||
|
||||
|
||||
Usage:
|
||||
config = MT5Config(
|
||||
login=12345678,
|
||||
@@ -70,22 +72,22 @@ class MT5Client:
|
||||
server="ICMarkets-Demo"
|
||||
)
|
||||
client = MT5Client(config)
|
||||
|
||||
|
||||
if client.connect():
|
||||
# Place order
|
||||
result = client.place_market_order("EURUSD", "buy", 0.1)
|
||||
|
||||
|
||||
# Get positions
|
||||
positions = client.get_positions()
|
||||
|
||||
|
||||
client.disconnect()
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, config: Optional[MT5Config] = None):
|
||||
self.config = config or MT5Config()
|
||||
self._connected = False
|
||||
self._lock = threading.Lock()
|
||||
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""Check if connected to MT5 terminal."""
|
||||
@@ -97,63 +99,65 @@ class MT5Client:
|
||||
return info is not None and info.connected
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""
|
||||
Connect to MT5 terminal.
|
||||
|
||||
|
||||
Returns:
|
||||
True if connected successfully
|
||||
"""
|
||||
with self._lock:
|
||||
if self.connected:
|
||||
return True
|
||||
|
||||
|
||||
try:
|
||||
_ensure_mt5()
|
||||
|
||||
|
||||
# Initialize MT5 connection
|
||||
init_params = {}
|
||||
|
||||
|
||||
if self.config.terminal_path:
|
||||
init_params["path"] = self.config.terminal_path
|
||||
|
||||
|
||||
if self.config.login and self.config.password and self.config.server:
|
||||
init_params["login"] = self.config.login
|
||||
init_params["password"] = self.config.password
|
||||
init_params["server"] = self.config.server
|
||||
init_params["timeout"] = self.config.timeout
|
||||
|
||||
|
||||
logger.info(f"Connecting to MT5: server={self.config.server}, login={self.config.login}")
|
||||
|
||||
|
||||
if init_params:
|
||||
initialized = mt5.initialize(**init_params)
|
||||
else:
|
||||
# Connect to already running terminal
|
||||
initialized = mt5.initialize()
|
||||
|
||||
|
||||
if not initialized:
|
||||
error = mt5.last_error()
|
||||
logger.error(f"MT5 initialization failed: {error}")
|
||||
return False
|
||||
|
||||
|
||||
self._connected = True
|
||||
|
||||
|
||||
# Log account info
|
||||
account_info = mt5.account_info()
|
||||
if account_info:
|
||||
logger.info(f"MT5 connected: account={account_info.login}, "
|
||||
f"server={account_info.server}, balance={account_info.balance}")
|
||||
logger.info(
|
||||
f"MT5 connected: account={account_info.login}, "
|
||||
f"server={account_info.server}, balance={account_info.balance}"
|
||||
)
|
||||
else:
|
||||
logger.warning("MT5 connected but account info not available")
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"MT5 connection failed: {e}")
|
||||
self._connected = False
|
||||
return False
|
||||
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from MT5 terminal."""
|
||||
with self._lock:
|
||||
@@ -166,15 +170,15 @@ class MT5Client:
|
||||
finally:
|
||||
self._connected = False
|
||||
logger.info("MT5 disconnected")
|
||||
|
||||
|
||||
def _ensure_connected(self):
|
||||
"""Ensure connection is established."""
|
||||
if not self.connected:
|
||||
if not self.connect():
|
||||
raise ConnectionError("Cannot connect to MT5 terminal")
|
||||
|
||||
|
||||
# ==================== Order Methods ====================
|
||||
|
||||
|
||||
def place_market_order(
|
||||
self,
|
||||
symbol: str,
|
||||
@@ -185,65 +189,54 @@ class MT5Client:
|
||||
) -> OrderResult:
|
||||
"""
|
||||
Place a market order.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Trading symbol (e.g., "EURUSD")
|
||||
side: Direction ("buy" or "sell")
|
||||
volume: Lot size (e.g., 0.1 = 1 mini lot)
|
||||
deviation: Maximum price deviation in points
|
||||
comment: Order comment
|
||||
|
||||
|
||||
Returns:
|
||||
OrderResult
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
_ensure_mt5()
|
||||
|
||||
|
||||
# Normalize symbol
|
||||
symbol = normalize_symbol(symbol)
|
||||
|
||||
|
||||
# Get symbol info
|
||||
symbol_info = mt5.symbol_info(symbol)
|
||||
if symbol_info is None:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Symbol not found: {symbol}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Symbol not found: {symbol}")
|
||||
|
||||
# Validate volume against symbol constraints
|
||||
volume_float = float(volume)
|
||||
if volume_float < symbol_info.volume_min:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Volume {volume_float} is less than minimum {symbol_info.volume_min}"
|
||||
success=False, message=f"Volume {volume_float} is less than minimum {symbol_info.volume_min}"
|
||||
)
|
||||
if volume_float > symbol_info.volume_max:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Volume {volume_float} exceeds maximum {symbol_info.volume_max}"
|
||||
success=False, message=f"Volume {volume_float} exceeds maximum {symbol_info.volume_max}"
|
||||
)
|
||||
# Round volume to lot step
|
||||
volume_step = symbol_info.volume_step
|
||||
if volume_step > 0:
|
||||
volume_float = round(volume_float / volume_step) * volume_step
|
||||
|
||||
|
||||
if not symbol_info.visible:
|
||||
# Enable symbol in Market Watch
|
||||
if not mt5.symbol_select(symbol, True):
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Failed to select symbol: {symbol}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Failed to select symbol: {symbol}")
|
||||
|
||||
# Get current price
|
||||
tick = mt5.symbol_info_tick(symbol)
|
||||
if tick is None:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Failed to get tick for: {symbol}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Failed to get tick for: {symbol}")
|
||||
|
||||
# Determine order type and price
|
||||
if side.lower() == "buy":
|
||||
order_type = mt5.ORDER_TYPE_BUY
|
||||
@@ -251,7 +244,7 @@ class MT5Client:
|
||||
else:
|
||||
order_type = mt5.ORDER_TYPE_SELL
|
||||
price = tick.bid
|
||||
|
||||
|
||||
# Determine filling mode based on symbol properties
|
||||
# Different brokers support different filling modes
|
||||
filling_mode = mt5.ORDER_FILLING_IOC # Default
|
||||
@@ -261,7 +254,7 @@ class MT5Client:
|
||||
filling_mode = mt5.ORDER_FILLING_FOK
|
||||
elif symbol_info.filling_mode & mt5.ORDER_FILLING_RETURN:
|
||||
filling_mode = mt5.ORDER_FILLING_RETURN
|
||||
|
||||
|
||||
# Prepare order request
|
||||
request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
@@ -275,26 +268,23 @@ class MT5Client:
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": filling_mode,
|
||||
}
|
||||
|
||||
|
||||
# Send order
|
||||
result = mt5.order_send(request)
|
||||
|
||||
|
||||
if result is None:
|
||||
error = mt5.last_error()
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Order send failed: {error}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Order send failed: {error}")
|
||||
|
||||
if result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
order_id=result.order if hasattr(result, 'order') else 0,
|
||||
order_id=result.order if hasattr(result, "order") else 0,
|
||||
status=str(result.retcode),
|
||||
message=f"Order rejected: {result.comment}",
|
||||
raw=result._asdict() if hasattr(result, '_asdict') else {}
|
||||
raw=result._asdict() if hasattr(result, "_asdict") else {},
|
||||
)
|
||||
|
||||
|
||||
return OrderResult(
|
||||
success=True,
|
||||
order_id=result.order,
|
||||
@@ -303,16 +293,13 @@ class MT5Client:
|
||||
price=result.price,
|
||||
status="filled",
|
||||
message="Order executed",
|
||||
raw=result._asdict() if hasattr(result, '_asdict') else {}
|
||||
raw=result._asdict() if hasattr(result, "_asdict") else {},
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Market order failed: {e}")
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=str(e)
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=str(e))
|
||||
|
||||
def place_limit_order(
|
||||
self,
|
||||
symbol: str,
|
||||
@@ -323,40 +310,34 @@ class MT5Client:
|
||||
) -> OrderResult:
|
||||
"""
|
||||
Place a pending limit order.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Trading symbol
|
||||
side: Direction ("buy" or "sell")
|
||||
volume: Lot size
|
||||
price: Limit price
|
||||
comment: Order comment
|
||||
|
||||
|
||||
Returns:
|
||||
OrderResult
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
_ensure_mt5()
|
||||
|
||||
|
||||
symbol = normalize_symbol(symbol)
|
||||
|
||||
|
||||
symbol_info = mt5.symbol_info(symbol)
|
||||
if symbol_info is None:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Symbol not found: {symbol}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Symbol not found: {symbol}")
|
||||
|
||||
if not symbol_info.visible:
|
||||
mt5.symbol_select(symbol, True)
|
||||
|
||||
|
||||
tick = mt5.symbol_info_tick(symbol)
|
||||
if tick is None:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Failed to get tick for: {symbol}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Failed to get tick for: {symbol}")
|
||||
|
||||
# Determine order type based on side and price relative to market
|
||||
if side.lower() == "buy":
|
||||
if price < tick.ask:
|
||||
@@ -368,7 +349,7 @@ class MT5Client:
|
||||
order_type = mt5.ORDER_TYPE_SELL_LIMIT
|
||||
else:
|
||||
order_type = mt5.ORDER_TYPE_SELL_STOP
|
||||
|
||||
|
||||
request = {
|
||||
"action": mt5.TRADE_ACTION_PENDING,
|
||||
"symbol": symbol,
|
||||
@@ -379,39 +360,33 @@ class MT5Client:
|
||||
"comment": comment,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
}
|
||||
|
||||
|
||||
result = mt5.order_send(request)
|
||||
|
||||
|
||||
if result is None:
|
||||
error = mt5.last_error()
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Order send failed: {error}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Order send failed: {error}")
|
||||
|
||||
if result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
status=str(result.retcode),
|
||||
message=f"Order rejected: {result.comment}",
|
||||
)
|
||||
|
||||
|
||||
return OrderResult(
|
||||
success=True,
|
||||
order_id=result.order,
|
||||
price=price,
|
||||
status="pending",
|
||||
message="Pending order placed",
|
||||
raw=result._asdict() if hasattr(result, '_asdict') else {}
|
||||
raw=result._asdict() if hasattr(result, "_asdict") else {},
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Limit order failed: {e}")
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=str(e)
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=str(e))
|
||||
|
||||
def close_position(
|
||||
self,
|
||||
ticket: int,
|
||||
@@ -421,47 +396,38 @@ class MT5Client:
|
||||
) -> OrderResult:
|
||||
"""
|
||||
Close an open position.
|
||||
|
||||
|
||||
Args:
|
||||
ticket: Position ticket number
|
||||
volume: Volume to close (None = close all)
|
||||
deviation: Maximum price deviation
|
||||
comment: Order comment
|
||||
|
||||
|
||||
Returns:
|
||||
OrderResult
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
_ensure_mt5()
|
||||
|
||||
|
||||
# Get position info
|
||||
position = mt5.positions_get(ticket=ticket)
|
||||
if not position:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Position not found: {ticket}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Position not found: {ticket}")
|
||||
|
||||
pos = position[0]
|
||||
symbol = pos.symbol
|
||||
|
||||
|
||||
# Get symbol info for filling mode
|
||||
symbol_info = mt5.symbol_info(symbol)
|
||||
if symbol_info is None:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Symbol not found: {symbol}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Symbol not found: {symbol}")
|
||||
|
||||
# Get tick
|
||||
tick = mt5.symbol_info_tick(symbol)
|
||||
if tick is None:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Failed to get tick for: {symbol}"
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=f"Failed to get tick for: {symbol}")
|
||||
|
||||
# Determine close direction and price
|
||||
if pos.type == mt5.POSITION_TYPE_BUY:
|
||||
order_type = mt5.ORDER_TYPE_SELL
|
||||
@@ -469,9 +435,9 @@ class MT5Client:
|
||||
else:
|
||||
order_type = mt5.ORDER_TYPE_BUY
|
||||
price = tick.ask
|
||||
|
||||
|
||||
close_volume = volume if volume else pos.volume
|
||||
|
||||
|
||||
# Determine filling mode based on symbol properties
|
||||
filling_mode = mt5.ORDER_FILLING_IOC # Default
|
||||
if symbol_info.filling_mode & mt5.ORDER_FILLING_IOC:
|
||||
@@ -480,7 +446,7 @@ class MT5Client:
|
||||
filling_mode = mt5.ORDER_FILLING_FOK
|
||||
elif symbol_info.filling_mode & mt5.ORDER_FILLING_RETURN:
|
||||
filling_mode = mt5.ORDER_FILLING_RETURN
|
||||
|
||||
|
||||
request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": symbol,
|
||||
@@ -494,15 +460,14 @@ class MT5Client:
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": filling_mode,
|
||||
}
|
||||
|
||||
|
||||
result = mt5.order_send(request)
|
||||
|
||||
|
||||
if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=f"Close failed: {result.comment if result else 'Unknown error'}"
|
||||
success=False, message=f"Close failed: {result.comment if result else 'Unknown error'}"
|
||||
)
|
||||
|
||||
|
||||
return OrderResult(
|
||||
success=True,
|
||||
order_id=result.order,
|
||||
@@ -512,63 +477,60 @@ class MT5Client:
|
||||
status="closed",
|
||||
message="Position closed",
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Close position failed: {e}")
|
||||
return OrderResult(
|
||||
success=False,
|
||||
message=str(e)
|
||||
)
|
||||
|
||||
return OrderResult(success=False, message=str(e))
|
||||
|
||||
def cancel_order(self, ticket: int) -> bool:
|
||||
"""
|
||||
Cancel a pending order.
|
||||
|
||||
|
||||
Args:
|
||||
ticket: Order ticket number
|
||||
|
||||
|
||||
Returns:
|
||||
True if cancelled successfully
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
_ensure_mt5()
|
||||
|
||||
|
||||
request = {
|
||||
"action": mt5.TRADE_ACTION_REMOVE,
|
||||
"order": ticket,
|
||||
}
|
||||
|
||||
|
||||
result = mt5.order_send(request)
|
||||
|
||||
|
||||
if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
logger.warning(f"Cancel order failed: {result.comment if result else 'Unknown'}")
|
||||
return False
|
||||
|
||||
|
||||
logger.info(f"Order {ticket} cancelled")
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cancel order failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ==================== Query Methods ====================
|
||||
|
||||
|
||||
def get_account_info(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get account information.
|
||||
|
||||
|
||||
Returns:
|
||||
Account info dictionary
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
_ensure_mt5()
|
||||
|
||||
|
||||
info = mt5.account_info()
|
||||
if info is None:
|
||||
return {"success": False, "error": "Failed to get account info"}
|
||||
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"login": info.login,
|
||||
@@ -585,79 +547,81 @@ class MT5Client:
|
||||
"trade_allowed": info.trade_allowed,
|
||||
"trade_expert": info.trade_expert,
|
||||
}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get account info failed: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
def get_positions(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get open positions.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Filter by symbol (optional)
|
||||
|
||||
|
||||
Returns:
|
||||
List of positions
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
_ensure_mt5()
|
||||
|
||||
|
||||
if symbol:
|
||||
positions = mt5.positions_get(symbol=normalize_symbol(symbol))
|
||||
else:
|
||||
positions = mt5.positions_get()
|
||||
|
||||
|
||||
if positions is None:
|
||||
return []
|
||||
|
||||
|
||||
result = []
|
||||
for pos in positions:
|
||||
result.append({
|
||||
"ticket": pos.ticket,
|
||||
"symbol": pos.symbol,
|
||||
"type": "buy" if pos.type == mt5.POSITION_TYPE_BUY else "sell",
|
||||
"volume": pos.volume,
|
||||
"price_open": pos.price_open,
|
||||
"price_current": pos.price_current,
|
||||
"sl": pos.sl,
|
||||
"tp": pos.tp,
|
||||
"profit": pos.profit,
|
||||
"swap": pos.swap,
|
||||
"magic": pos.magic,
|
||||
"comment": pos.comment,
|
||||
"time": datetime.fromtimestamp(pos.time).isoformat(),
|
||||
})
|
||||
|
||||
result.append(
|
||||
{
|
||||
"ticket": pos.ticket,
|
||||
"symbol": pos.symbol,
|
||||
"type": "buy" if pos.type == mt5.POSITION_TYPE_BUY else "sell",
|
||||
"volume": pos.volume,
|
||||
"price_open": pos.price_open,
|
||||
"price_current": pos.price_current,
|
||||
"sl": pos.sl,
|
||||
"tp": pos.tp,
|
||||
"profit": pos.profit,
|
||||
"swap": pos.swap,
|
||||
"magic": pos.magic,
|
||||
"comment": pos.comment,
|
||||
"time": datetime.fromtimestamp(pos.time).isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get positions failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get pending orders.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Filter by symbol (optional)
|
||||
|
||||
|
||||
Returns:
|
||||
List of orders
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
_ensure_mt5()
|
||||
|
||||
|
||||
if symbol:
|
||||
orders = mt5.orders_get(symbol=normalize_symbol(symbol))
|
||||
else:
|
||||
orders = mt5.orders_get()
|
||||
|
||||
|
||||
if orders is None:
|
||||
return []
|
||||
|
||||
|
||||
result = []
|
||||
for order in orders:
|
||||
order_type_map = {
|
||||
@@ -666,56 +630,58 @@ class MT5Client:
|
||||
mt5.ORDER_TYPE_BUY_STOP: "buy_stop",
|
||||
mt5.ORDER_TYPE_SELL_STOP: "sell_stop",
|
||||
}
|
||||
|
||||
result.append({
|
||||
"ticket": order.ticket,
|
||||
"symbol": order.symbol,
|
||||
"type": order_type_map.get(order.type, str(order.type)),
|
||||
"volume_initial": order.volume_initial,
|
||||
"volume_current": order.volume_current,
|
||||
"price_open": order.price_open,
|
||||
"price_current": order.price_current,
|
||||
"sl": order.sl,
|
||||
"tp": order.tp,
|
||||
"magic": order.magic,
|
||||
"comment": order.comment,
|
||||
"time_setup": datetime.fromtimestamp(order.time_setup).isoformat(),
|
||||
})
|
||||
|
||||
|
||||
result.append(
|
||||
{
|
||||
"ticket": order.ticket,
|
||||
"symbol": order.symbol,
|
||||
"type": order_type_map.get(order.type, str(order.type)),
|
||||
"volume_initial": order.volume_initial,
|
||||
"volume_current": order.volume_current,
|
||||
"price_open": order.price_open,
|
||||
"price_current": order.price_current,
|
||||
"sl": order.sl,
|
||||
"tp": order.tp,
|
||||
"magic": order.magic,
|
||||
"comment": order.comment,
|
||||
"time_setup": datetime.fromtimestamp(order.time_setup).isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get orders failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_quote(self, symbol: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get real-time quote.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Symbol code
|
||||
|
||||
|
||||
Returns:
|
||||
Quote data
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
_ensure_mt5()
|
||||
|
||||
|
||||
symbol = normalize_symbol(symbol)
|
||||
|
||||
|
||||
# Select symbol
|
||||
symbol_info = mt5.symbol_info(symbol)
|
||||
if symbol_info is None:
|
||||
return {"success": False, "error": f"Symbol not found: {symbol}"}
|
||||
|
||||
|
||||
if not symbol_info.visible:
|
||||
mt5.symbol_select(symbol, True)
|
||||
|
||||
|
||||
tick = mt5.symbol_info_tick(symbol)
|
||||
if tick is None:
|
||||
return {"success": False, "error": f"Failed to get tick: {symbol}"}
|
||||
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"symbol": symbol,
|
||||
@@ -726,58 +692,60 @@ class MT5Client:
|
||||
"time": datetime.fromtimestamp(tick.time).isoformat(),
|
||||
"spread": round((tick.ask - tick.bid) / symbol_info.point, 1),
|
||||
}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get quote failed: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
def get_symbols(self, group: str = "*") -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get available symbols.
|
||||
|
||||
|
||||
Args:
|
||||
group: Filter by group pattern (e.g., "*USD*", "Forex*")
|
||||
|
||||
|
||||
Returns:
|
||||
List of symbol info
|
||||
"""
|
||||
try:
|
||||
self._ensure_connected()
|
||||
_ensure_mt5()
|
||||
|
||||
|
||||
symbols = mt5.symbols_get(group=group)
|
||||
if symbols is None:
|
||||
return []
|
||||
|
||||
|
||||
result = []
|
||||
for s in symbols:
|
||||
result.append({
|
||||
"name": s.name,
|
||||
"description": s.description,
|
||||
"path": s.path,
|
||||
"currency_base": s.currency_base,
|
||||
"currency_profit": s.currency_profit,
|
||||
"digits": s.digits,
|
||||
"point": s.point,
|
||||
"trade_mode": s.trade_mode,
|
||||
"volume_min": s.volume_min,
|
||||
"volume_max": s.volume_max,
|
||||
"volume_step": s.volume_step,
|
||||
})
|
||||
|
||||
result.append(
|
||||
{
|
||||
"name": s.name,
|
||||
"description": s.description,
|
||||
"path": s.path,
|
||||
"currency_base": s.currency_base,
|
||||
"currency_profit": s.currency_profit,
|
||||
"digits": s.digits,
|
||||
"point": s.point,
|
||||
"trade_mode": s.trade_mode,
|
||||
"volume_min": s.volume_min,
|
||||
"volume_max": s.volume_max,
|
||||
"volume_step": s.volume_step,
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get symbols failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_connection_status(self) -> Dict[str, Any]:
|
||||
"""Get connection status."""
|
||||
try:
|
||||
_ensure_mt5()
|
||||
terminal_info = mt5.terminal_info() if self._connected else None
|
||||
account_info = mt5.account_info() if self._connected else None
|
||||
|
||||
|
||||
return {
|
||||
"connected": self.connected,
|
||||
"login": self.config.login,
|
||||
@@ -802,15 +770,15 @@ _global_lock = threading.Lock()
|
||||
def get_mt5_client(config: Optional[MT5Config] = None) -> MT5Client:
|
||||
"""
|
||||
Get global MT5 client singleton.
|
||||
|
||||
|
||||
Args:
|
||||
config: Configuration (only effective on first call)
|
||||
|
||||
|
||||
Returns:
|
||||
MT5Client instance
|
||||
"""
|
||||
global _global_client
|
||||
|
||||
|
||||
with _global_lock:
|
||||
if _global_client is None:
|
||||
_global_client = MT5Client(config)
|
||||
@@ -820,7 +788,7 @@ def get_mt5_client(config: Optional[MT5Config] = None) -> MT5Client:
|
||||
def reset_mt5_client():
|
||||
"""Reset global client (disconnect and clear instance)."""
|
||||
global _global_client
|
||||
|
||||
|
||||
with _global_lock:
|
||||
if _global_client is not None:
|
||||
_global_client.disconnect()
|
||||
|
||||
@@ -4,90 +4,135 @@ Symbol Mapping and Conversion for MT5
|
||||
Handles forex symbol normalization and parsing.
|
||||
"""
|
||||
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# Common forex pairs with their typical MT5 symbol format
|
||||
FOREX_PAIRS = {
|
||||
# Major pairs
|
||||
"EURUSD", "GBPUSD", "USDJPY", "USDCHF", "AUDUSD", "USDCAD", "NZDUSD",
|
||||
"EURUSD",
|
||||
"GBPUSD",
|
||||
"USDJPY",
|
||||
"USDCHF",
|
||||
"AUDUSD",
|
||||
"USDCAD",
|
||||
"NZDUSD",
|
||||
# Cross pairs
|
||||
"EURGBP", "EURJPY", "EURCHF", "EURAUD", "EURCAD", "EURNZD",
|
||||
"GBPJPY", "GBPCHF", "GBPAUD", "GBPCAD", "GBPNZD",
|
||||
"AUDJPY", "AUDCHF", "AUDCAD", "AUDNZD",
|
||||
"NZDJPY", "NZDCHF", "NZDCAD",
|
||||
"CADJPY", "CADCHF",
|
||||
"EURGBP",
|
||||
"EURJPY",
|
||||
"EURCHF",
|
||||
"EURAUD",
|
||||
"EURCAD",
|
||||
"EURNZD",
|
||||
"GBPJPY",
|
||||
"GBPCHF",
|
||||
"GBPAUD",
|
||||
"GBPCAD",
|
||||
"GBPNZD",
|
||||
"AUDJPY",
|
||||
"AUDCHF",
|
||||
"AUDCAD",
|
||||
"AUDNZD",
|
||||
"NZDJPY",
|
||||
"NZDCHF",
|
||||
"NZDCAD",
|
||||
"CADJPY",
|
||||
"CADCHF",
|
||||
"CHFJPY",
|
||||
# Exotic pairs
|
||||
"USDMXN", "USDZAR", "USDTRY", "USDHKD", "USDSGD", "USDNOK", "USDSEK", "USDDKK",
|
||||
"EURTRY", "EURMXN", "EURNOK", "EURSEK", "EURDKK", "EURPLN", "EURHUF", "EURCZK",
|
||||
"USDMXN",
|
||||
"USDZAR",
|
||||
"USDTRY",
|
||||
"USDHKD",
|
||||
"USDSGD",
|
||||
"USDNOK",
|
||||
"USDSEK",
|
||||
"USDDKK",
|
||||
"EURTRY",
|
||||
"EURMXN",
|
||||
"EURNOK",
|
||||
"EURSEK",
|
||||
"EURDKK",
|
||||
"EURPLN",
|
||||
"EURHUF",
|
||||
"EURCZK",
|
||||
# Metals
|
||||
"XAUUSD", "XAGUSD", "XAUEUR",
|
||||
"XAUUSD",
|
||||
"XAGUSD",
|
||||
"XAUEUR",
|
||||
# Indices (CFD)
|
||||
"US30", "US500", "USTEC", "UK100", "DE40", "JP225", "AU200",
|
||||
"US30",
|
||||
"US500",
|
||||
"USTEC",
|
||||
"UK100",
|
||||
"DE40",
|
||||
"JP225",
|
||||
"AU200",
|
||||
# Crypto (if broker supports)
|
||||
"BTCUSD", "ETHUSD", "LTCUSD", "XRPUSD",
|
||||
"BTCUSD",
|
||||
"ETHUSD",
|
||||
"LTCUSD",
|
||||
"XRPUSD",
|
||||
}
|
||||
|
||||
|
||||
def normalize_symbol(symbol: str, broker_suffix: str = "") -> str:
|
||||
"""
|
||||
Normalize symbol to MT5 format.
|
||||
|
||||
|
||||
Different brokers may use different suffixes:
|
||||
- No suffix: "EURUSD"
|
||||
- With suffix: "EURUSDm", "EURUSD.raw", "EURUSD-ECN"
|
||||
|
||||
|
||||
Args:
|
||||
symbol: Symbol code (e.g., "EUR/USD", "EURUSD", "eurusd")
|
||||
broker_suffix: Broker-specific suffix (e.g., "m", ".raw", "-ECN")
|
||||
|
||||
|
||||
Returns:
|
||||
Normalized MT5 symbol
|
||||
"""
|
||||
# Remove common separators and convert to uppercase
|
||||
normalized = (symbol or "").strip().upper()
|
||||
normalized = normalized.replace("/", "").replace("-", "").replace("_", "").replace(" ", "")
|
||||
|
||||
|
||||
# Add broker suffix if provided
|
||||
if broker_suffix:
|
||||
# Check if symbol already has the suffix
|
||||
if not normalized.endswith(broker_suffix.upper()):
|
||||
normalized = normalized + broker_suffix
|
||||
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def parse_symbol(symbol: str) -> Tuple[str, Optional[str]]:
|
||||
"""
|
||||
Parse symbol and extract base/quote currencies.
|
||||
|
||||
|
||||
Args:
|
||||
symbol: MT5 symbol (e.g., "EURUSD", "EURUSDm")
|
||||
|
||||
|
||||
Returns:
|
||||
(clean_symbol, market_type)
|
||||
"""
|
||||
clean = (symbol or "").strip().upper()
|
||||
|
||||
|
||||
# Remove common broker suffixes
|
||||
for suffix in ["M", ".RAW", "-ECN", ".STD", ".PRO", ".", "#"]:
|
||||
if clean.endswith(suffix):
|
||||
clean = clean[:-len(suffix)]
|
||||
|
||||
clean = clean[: -len(suffix)]
|
||||
|
||||
# Determine market type based on symbol pattern
|
||||
if clean in FOREX_PAIRS or (len(clean) == 6 and clean.isalpha()):
|
||||
return clean, "forex"
|
||||
|
||||
|
||||
if clean.startswith("XAU") or clean.startswith("XAG"):
|
||||
return clean, "metal"
|
||||
|
||||
|
||||
if clean.startswith("BTC") or clean.startswith("ETH") or clean.startswith("LTC"):
|
||||
return clean, "crypto"
|
||||
|
||||
|
||||
if any(idx in clean for idx in ["US30", "US500", "USTEC", "UK100", "DE40", "JP225"]):
|
||||
return clean, "index"
|
||||
|
||||
|
||||
# Default to forex
|
||||
return clean, "forex"
|
||||
|
||||
@@ -95,21 +140,21 @@ def parse_symbol(symbol: str) -> Tuple[str, Optional[str]]:
|
||||
def get_lot_size_info(symbol: str) -> dict:
|
||||
"""
|
||||
Get lot size information for a symbol.
|
||||
|
||||
|
||||
Standard forex lot sizes:
|
||||
- Standard lot: 100,000 units
|
||||
- Mini lot: 10,000 units
|
||||
- Micro lot: 1,000 units
|
||||
- Nano lot: 100 units
|
||||
|
||||
|
||||
Args:
|
||||
symbol: MT5 symbol
|
||||
|
||||
|
||||
Returns:
|
||||
Dict with lot size information
|
||||
"""
|
||||
clean, market_type = parse_symbol(symbol)
|
||||
|
||||
|
||||
if market_type == "forex":
|
||||
return {
|
||||
"standard_lot": 100000,
|
||||
@@ -117,7 +162,7 @@ def get_lot_size_info(symbol: str) -> dict:
|
||||
"lot_step": 0.01,
|
||||
"max_lot": 100.0,
|
||||
}
|
||||
|
||||
|
||||
if market_type == "metal":
|
||||
# Gold/Silver typically uses oz
|
||||
return {
|
||||
@@ -126,7 +171,7 @@ def get_lot_size_info(symbol: str) -> dict:
|
||||
"lot_step": 0.01,
|
||||
"max_lot": 50.0,
|
||||
}
|
||||
|
||||
|
||||
if market_type == "index":
|
||||
return {
|
||||
"standard_lot": 1,
|
||||
@@ -134,7 +179,7 @@ def get_lot_size_info(symbol: str) -> dict:
|
||||
"lot_step": 0.1,
|
||||
"max_lot": 100.0,
|
||||
}
|
||||
|
||||
|
||||
# Default
|
||||
return {
|
||||
"standard_lot": 1,
|
||||
|
||||
Reference in New Issue
Block a user