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:
dienakdz
2026-04-09 14:30:51 +07:00
parent 103055b3df
commit 87f2845483
157 changed files with 19026 additions and 17773 deletions
@@ -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,