├── tests ├── __init__.py ├── test_signal.py ├── conftest.py ├── test_chart.py ├── test_error.py ├── test_symbol.py ├── test_account.py └── test_order.py ├── mt4client ├── __init__.py ├── api │ ├── __init__.py │ ├── signal.py │ ├── chart.py │ ├── order.py │ ├── account.py │ ├── errors.py │ └── symbol.py └── client.py ├── setup.py ├── .idea ├── vcs.xml ├── .gitignore ├── inspectionProfiles │ └── profiles_settings.xml ├── modules.xml ├── dictionaries │ └── Joe.xml ├── misc.xml └── metatrader4-client-python.iml ├── Pipfile ├── README.md └── Pipfile.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mt4client/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import MT4Client 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name="mt4client", packages=find_packages()) 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | pyzmq = "*" 10 | pytest = "*" 11 | py = "==1.10.0" 12 | 13 | [requires] 14 | python_version = "3.8.3" 15 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/dictionaries/Joe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | advisor 5 | eurusd 6 | ohlcv 7 | pytest 8 | stopout 9 | timeframe 10 | timeframes 11 | 12 | 13 | -------------------------------------------------------------------------------- /mt4client/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .account import Account, AccountInfoDouble, AccountInfoInteger, AccountStopoutMode, AccountTradeMode 2 | from .chart import StandardTimeframe, NonStandardTimeframe, parse_timeframe, OHLCV 3 | from .errors import MT4Error 4 | from .symbol import Symbol, SymbolTick, SymbolInfoInteger, SymbolCalcMode, SymbolTradeMode, SymbolTradeExecution, \ 5 | SymbolSwapMode, DayOfWeek 6 | from .signal import Signal 7 | from .order import Order, OrderType 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /tests/test_signal.py: -------------------------------------------------------------------------------- 1 | from mt4client import MT4Client 2 | 3 | 4 | def test_signal_names(mt4: MT4Client): 5 | signal_names = mt4.signal_names() 6 | assert isinstance(signal_names, list) 7 | assert len(signal_names) > 0 8 | print(f"Found {len(signal_names)} signal names. The first one: {signal_names[0]}") 9 | 10 | 11 | def test_signals(mt4: MT4Client): 12 | signals = mt4.signals(*mt4.signal_names()[0:3]) 13 | assert isinstance(signals, dict) 14 | assert len(signals) == 3 15 | print(f"Found {len(signals)} signals. The first one: {next(iter(signals.items()))}") 16 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Pytest config for integration tests.""" 2 | 3 | import pytest 4 | from mt4client import MT4Client 5 | from mt4client.api import Symbol 6 | 7 | 8 | @pytest.fixture(scope="session", autouse=True) 9 | def mt4() -> MT4Client: 10 | mt4 = MT4Client(address="tcp://win10:28282", verbose=False) 11 | yield mt4 12 | mt4.shutdown() 13 | 14 | 15 | @pytest.fixture(scope="session", autouse=True) 16 | def symbol_name() -> str: 17 | return "EURUSD" 18 | 19 | 20 | @pytest.fixture(scope="session") 21 | def symbol(mt4: MT4Client, symbol_name: str) -> Symbol: 22 | return mt4.symbol(symbol_name) 23 | -------------------------------------------------------------------------------- /.idea/metatrader4-client-python.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /tests/test_chart.py: -------------------------------------------------------------------------------- 1 | """Unit tests for chart-related functions.""" 2 | 3 | from mt4client.api import parse_timeframe, StandardTimeframe, NonStandardTimeframe 4 | 5 | 6 | def test_parse_timeframe(): 7 | assert parse_timeframe("15m") == StandardTimeframe.PERIOD_M15 8 | assert parse_timeframe("1h") == StandardTimeframe.PERIOD_H1 9 | assert parse_timeframe("1d") == StandardTimeframe.PERIOD_D1 10 | assert parse_timeframe("1w") == StandardTimeframe.PERIOD_W1 11 | assert parse_timeframe("1mn") == StandardTimeframe.PERIOD_MN1 12 | assert parse_timeframe("0") == StandardTimeframe.PERIOD_CURRENT 13 | assert parse_timeframe("2m") == NonStandardTimeframe.PERIOD_M2 14 | assert parse_timeframe("2h") == NonStandardTimeframe.PERIOD_H2 15 | 16 | -------------------------------------------------------------------------------- /tests/test_error.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mt4client import MT4Client 3 | from mt4client.api import Symbol, OrderType, MT4Error 4 | from mt4client.api.errors import MT4ErrorCode 5 | 6 | 7 | def test_retry_on_error(mt4: MT4Client, symbol: Symbol): 8 | # this should raise ERR_INVALID_PRICE_PARAM 9 | invalid_price = -10.0 10 | try: 11 | order = mt4.order_send( 12 | symbol=symbol, 13 | lots=symbol.volume_min, 14 | order_type=OrderType.OP_BUYLIMIT, 15 | price=invalid_price 16 | ) 17 | pytest.fail(f"Expected invalidly priced order to fail:\n\t{order}") 18 | except MT4Error as ex: 19 | if ex.error_code is MT4ErrorCode.ERR_INVALID_PRICE_PARAM: 20 | # resend the order with a valid price 21 | new_price = symbol.tick.ask 22 | order = mt4.order_send( 23 | symbol=symbol, 24 | lots=symbol.volume_min, 25 | order_type=OrderType.OP_BUYLIMIT, 26 | price=new_price 27 | ) 28 | assert order is not None 29 | assert order.open_price == new_price 30 | print(f"Order resend was successful: {order}") 31 | else: 32 | pytest.fail(f"Expected ERR_INVALID_PRICE_PARAM error instead of:\n\t{ex}") 33 | -------------------------------------------------------------------------------- /tests/test_symbol.py: -------------------------------------------------------------------------------- 1 | """Integration tests for the symbol-related functions.""" 2 | 3 | import pytest 4 | from enum import Enum 5 | 6 | from mt4client.api import Symbol, StandardTimeframe 7 | from mt4client import MT4Client 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def symbol(mt4: MT4Client, symbol_name: str) -> Symbol: 12 | return mt4.symbol(symbol_name) 13 | 14 | 15 | def test_symbol_tick(symbol: Symbol): 16 | tick = symbol.tick 17 | assert isinstance(tick.time, int) 18 | assert isinstance(tick.bid, float) 19 | assert isinstance(tick.ask, float) 20 | assert isinstance(tick.last, float) 21 | assert isinstance(tick.volume, int) 22 | 23 | 24 | def test_fetch_ohlcv(symbol: Symbol): 25 | ohlcv = symbol.ohlcv("1h", 100) 26 | assert isinstance(ohlcv, list) 27 | assert len(ohlcv) == 100 28 | print(f"Found {len(ohlcv)} OHLCV bars. The first one: {ohlcv[0]}") 29 | 30 | 31 | def test_symbol_names(mt4: MT4Client): 32 | symbol_names = mt4.symbol_names() 33 | assert isinstance(symbol_names, list) 34 | print(f"All symbols: {symbol_names}") 35 | 36 | 37 | def test_symbols(mt4: MT4Client): 38 | symbols = mt4.symbols(*mt4.symbol_names()[0:3]) 39 | assert isinstance(symbols, dict) 40 | assert len(symbols) == 3 41 | print(f"Found {len(symbols)} symbols. The first one: {next(iter(symbols.items()))}") 42 | 43 | 44 | def test_indicator(mt4: MT4Client, symbol: Symbol): 45 | func = "iAC" 46 | args = [symbol.name, StandardTimeframe.PERIOD_H1.value, 1] 47 | result = mt4.indicator(func, args) 48 | assert isinstance(result, float) 49 | print(f"{func}({str(args)[1:-1]}) = {result}") 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MetaTrader 4 Python Client 2 | A Python 3 client and API for the [MetaTrader 4 server](https://github.com/CoeJoder/metatrader4-server). 3 | 4 | ## Usage 5 | ```python 6 | from mt4client import MT4Client 7 | 8 | # create ZeroMQ socket and connect to server 9 | mt4 = MT4Client(address="tcp://mt4server:28282") 10 | 11 | # query the MetaTrader terminal 12 | account = mt4.account() 13 | signals = mt4.signal_names() 14 | symbols = mt4.symbol_names() 15 | first_signal = mt4.signal(signals[0]) 16 | iac = mt4.indicator("iAC", ["EURUSD", 60, 1]) 17 | eur_usd = mt4.symbol("EURUSD") 18 | bars = eur_usd.ohlcv(timeframe="1h") 19 | 20 | print(f"I have {account.balance} {account.currency} to trade against {len(symbols)} symbols.") 21 | print(f"\n{eur_usd.name} symbol:\n\t{eur_usd}") 22 | print(f"\n\tThe latest tick is:\n\t{eur_usd.tick}") 23 | print(f"\n\tThe latest of {len(bars)} bars is:\n\t{bars[-1]}") 24 | print(f"\nThere are {len(signals)} signals, one of which is:\n{first_signal}") 25 | print(f"\nIndicator example:\niAC(EURUSD, 60, 1) = {iac}") 26 | ``` 27 | ```commandline 28 | > I have 768.66 USD to trade against 173 symbols. 29 | > 30 | > EURUSD symbol: 31 | > Symbol(name=EURUSD, point_size=1e-05, digits=5, lot_size=100000.0, tick_value=1.0, tick_size=1e-05, min_lot=0.01, lot_step=0.01, max_lot=1000.0, margin_init=0.0, margin_maintenance=0.0, margin_hedged=0.0, margin_required=1183.65, freeze_level=0.0) 32 | > 33 | > The latest tick is: 34 | > SymbolTick(time=1596206942, bid=1.18357, ask=1.18365, last=0.0, volume=0) 35 | > 36 | > The latest of 100 bars is: 37 | > OHLCV(open=1.18498, high=1.18503, low=1.18273, close=1.18357, tick_volume=2981, time=1596204000) 38 | > 39 | > There are 1000 signals, one of which is: 40 | > Signal(author_login=Antonov-EA, broker=GBE Trading Technology Ltd, broker_server=GBEbrokers-Demo, name=FibonacciBreakout, currency=EUR, date_published=1386197632, date_started=1386194045, id=21457, leverage=200, pips=-49736, rating=789, subscribers=13, trades=9869, trade_mode=, balance=21522.68, equity=21506.7, gain=115.08, max_drawdown=57.01, price=0.0, roi=114.9) 41 | > 42 | > Indicator example: 43 | > iAC(EURUSD, 60, 1) = -0.00173214 44 | ``` 45 | -------------------------------------------------------------------------------- /mt4client/api/signal.py: -------------------------------------------------------------------------------- 1 | from mt4client.api import AccountTradeMode 2 | 3 | 4 | class Signal: 5 | """A trading signal in MetaTrader 4. 6 | 7 | References: 8 | https://docs.mql4.com/signals 9 | 10 | https://docs.mql4.com/constants/tradingconstants/signalproperties 11 | """ 12 | 13 | def __init__(self, author_login: str, broker: str, broker_server: str, name: str, 14 | currency: str, date_published: int, date_started: int, id: int, leverage: int, pips: int, rating: int, 15 | subscribers: int, trades: int, trade_mode: int, balance: float, equity: float, gain: float, 16 | max_drawdown: float, price: float, roi: float): 17 | self.author_login = author_login 18 | """Author login.""" 19 | 20 | self.broker = broker 21 | """Broker name (company).""" 22 | 23 | self.broker_server = broker_server 24 | """Broker server.""" 25 | 26 | self.name = name 27 | """Signal name.""" 28 | 29 | self.currency = currency 30 | """Signal base currency.""" 31 | 32 | self.date_published = date_published 33 | """Publication date (date when it become available for subscription).""" 34 | 35 | self.date_started = date_started 36 | """Monitoring start date.""" 37 | 38 | self.id = id 39 | """Signal ID.""" 40 | 41 | self.leverage = leverage 42 | """Account leverage.""" 43 | 44 | self.pips = pips 45 | """Profit in pips.""" 46 | 47 | self.rating = rating 48 | """Position iin rating.""" 49 | 50 | self.subscribers = subscribers 51 | """Number of subscribers.""" 52 | 53 | self.trades = trades 54 | """Number of trades.""" 55 | 56 | self.trade_mode: AccountTradeMode = AccountTradeMode(trade_mode) 57 | """Account type.""" 58 | 59 | self.balance = balance 60 | """Account balance.""" 61 | 62 | self.equity = equity 63 | """Account equity.""" 64 | 65 | self.gain = gain 66 | """Account gain.""" 67 | 68 | self.max_drawdown = max_drawdown 69 | """Account maximum drawdown.""" 70 | 71 | self.price = price 72 | """Signal subscription price.""" 73 | 74 | self.roi = roi 75 | """Return on investment (%).""" 76 | 77 | def __repr__(self): 78 | return (f'{self.__class__.__name__}(' 79 | f'author_login={self.author_login}, ' 80 | f'broker={self.broker}, ' 81 | f'broker_server={self.broker_server}, ' 82 | f'name={self.name}, ' 83 | f'currency={self.currency}, ' 84 | f'date_published={self.date_published}, ' 85 | f'date_started={self.date_started}, ' 86 | f'id={self.id}, ' 87 | f'leverage={self.leverage}, ' 88 | f'pips={self.pips}, ' 89 | f'rating={self.rating}, ' 90 | f'subscribers={self.subscribers}, ' 91 | f'trades={self.trades}, ' 92 | f'trade_mode={self.trade_mode!r}, ' 93 | f'balance={self.balance}, ' 94 | f'equity={self.equity}, ' 95 | f'gain={self.gain}, ' 96 | f'max_drawdown={self.max_drawdown}, ' 97 | f'price={self.price}, ' 98 | f'roi={self.roi})') 99 | -------------------------------------------------------------------------------- /mt4client/api/chart.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Union 3 | import re 4 | 5 | 6 | class StandardTimeframe(Enum): 7 | """The standard timeframes available for chart data in MetaTrader 4. 8 | Online charts of financial instruments can be plotted only on these time intervals. 9 | 10 | Reference: 11 | https://docs.mql4.com/constants/chartconstants/enum_timeframes 12 | """ 13 | PERIOD_CURRENT = 0 14 | PERIOD_M1 = 1 15 | PERIOD_M5 = 5 16 | PERIOD_M15 = 15 17 | PERIOD_M30 = 30 18 | PERIOD_H1 = 60 19 | PERIOD_H4 = 240 20 | PERIOD_D1 = 1440 21 | PERIOD_W1 = 10080 22 | PERIOD_MN1 = 43200 23 | 24 | @property 25 | def is_standard(self) -> bool: 26 | return True 27 | 28 | 29 | class NonStandardTimeframe(Enum): 30 | """The non-standard timeframes available for chart data in MetaTrader 4. 31 | These periods can be used for working with offline charts. 32 | 33 | Reference: 34 | https://docs.mql4.com/constants/chartconstants/enum_timeframes 35 | """ 36 | PERIOD_M2 = 2 37 | PERIOD_M3 = 3 38 | PERIOD_M4 = 4 39 | PERIOD_M6 = 6 40 | PERIOD_M10 = 10 41 | PERIOD_M12 = 12 42 | PERIOD_M20 = 20 43 | PERIOD_H2 = 120 44 | PERIOD_H3 = 180 45 | PERIOD_H6 = 360 46 | PERIOD_H8 = 480 47 | PERIOD_H12 = 720 48 | 49 | @property 50 | def is_standard(self) -> bool: 51 | return False 52 | 53 | 54 | class OHLCV(object): 55 | """ 56 | An OHLCV bar. 57 | 58 | References: 59 | https://docs.mql4.com/series/copyrates 60 | https://docs.mql4.com/constants/structures/mqlrates 61 | """ 62 | 63 | def __init__(self, open: int, high: int, low: int, close: int, tick_volume: int, time: int): 64 | self.open = open 65 | """The period start time.""" 66 | 67 | self.high = high 68 | """The open price.""" 69 | 70 | self.low = low 71 | """The highest price of the period.""" 72 | 73 | self.close = close 74 | """The lowest price of the period.""" 75 | 76 | self.tick_volume = tick_volume 77 | """The close price.""" 78 | 79 | self.time = time 80 | """The tick volume.""" 81 | 82 | def __repr__(self): 83 | return (f'{self.__class__.__name__}(' 84 | f'open={self.open}, ' 85 | f'high={self.high}, ' 86 | f'low={self.low}, ' 87 | f'close={self.close}, ' 88 | f'tick_volume={self.tick_volume}, ' 89 | f'time={self.time})') 90 | 91 | 92 | def parse_timeframe(timeframe: str) -> Union[StandardTimeframe, NonStandardTimeframe]: 93 | """ 94 | Parses a timeframe string. An exception is raised if timeframe is invalid or parsing fails. 95 | 96 | :param timeframe: A timeframe string, eg. '10m', '4h', '1d', '1w', '1mn' etc. 97 | :return: A valid timeframe object. 98 | """ 99 | if timeframe == "0": 100 | return StandardTimeframe.PERIOD_CURRENT 101 | 102 | p = re.compile(r"(\d+)([mhdwn]+)", re.IGNORECASE) 103 | m = p.match(timeframe) 104 | if m is not None and len(m.groups()) == 2: 105 | name = "PERIOD_" + m.group(2).upper() + m.group(1) 106 | try: 107 | return StandardTimeframe[name] 108 | except KeyError: 109 | try: 110 | return NonStandardTimeframe[name] 111 | except KeyError: 112 | pass 113 | raise ValueError("Invalid timeframe: " + timeframe) 114 | -------------------------------------------------------------------------------- /tests/test_account.py: -------------------------------------------------------------------------------- 1 | """Integration tests for the account-related functions.""" 2 | 3 | import pytest 4 | 5 | from mt4client.api import Account, AccountTradeMode, AccountStopoutMode 6 | from mt4client import MT4Client 7 | 8 | 9 | @pytest.fixture(scope="module") 10 | def account(mt4: MT4Client) -> Account: 11 | return mt4.account() 12 | 13 | 14 | def test_account_login(account: Account): 15 | val = account.login 16 | assert isinstance(val, int) 17 | print(f"Account login: {val}") 18 | 19 | 20 | def test_account_trade_mode(account: Account): 21 | val = account.trade_mode 22 | assert isinstance(val, AccountTradeMode) 23 | print(f"Account trade_mode: {val}") 24 | 25 | 26 | def test_account_leverage(account: Account): 27 | val = account.leverage 28 | assert isinstance(val, int) 29 | print(f"Account leverage: {val}") 30 | 31 | 32 | def test_account_limit_orders(account: Account): 33 | val = account.limit_orders 34 | assert isinstance(val, int) 35 | print(f"Account limit_orders: {val}") 36 | 37 | 38 | def test_account_margin_so_mode(account: Account): 39 | val = account.margin_so_mode 40 | assert isinstance(val, AccountStopoutMode) 41 | print(f"Account margin_so_mode: {val}") 42 | 43 | 44 | def test_account_trade_allowed(account: Account): 45 | val = account.trade_allowed 46 | assert isinstance(val, bool) 47 | print(f"Account trade_allowed: {val}") 48 | 49 | 50 | def test_account_trade_expert(account: Account): 51 | val = account.trade_expert 52 | assert isinstance(val, int) 53 | print(f"Account trade_expert: {val}") 54 | 55 | 56 | def test_account_balance(account: Account): 57 | val = account.balance 58 | assert isinstance(val, float) 59 | print(f"Account balance: {val}") 60 | 61 | 62 | def test_account_credit(account: Account): 63 | val = account.credit 64 | assert isinstance(val, float) 65 | print(f"Account credit: {val}") 66 | 67 | 68 | def test_account_profit(account: Account): 69 | val = account.profit 70 | assert isinstance(val, float) 71 | print(f"Account profit: {val}") 72 | 73 | 74 | def test_account_equity(account: Account): 75 | val = account.equity 76 | assert isinstance(val, float) 77 | print(f"Account equity: {val}") 78 | 79 | 80 | def test_account_margin(account: Account): 81 | val = account.margin 82 | assert isinstance(val, float) 83 | print(f"Account margin: {val}") 84 | 85 | 86 | def test_account_margin_free(account: Account): 87 | val = account.margin_free 88 | assert isinstance(val, float) 89 | print(f"Account margin_free: {val}") 90 | 91 | 92 | def test_account_margin_level(account: Account): 93 | val = account.margin_level 94 | assert isinstance(val, float) 95 | print(f"Account margin_level: {val}") 96 | 97 | 98 | def test_account_margin_call_level(account: Account): 99 | val = account.margin_call_level 100 | assert isinstance(val, float) 101 | print(f"Account margin_call_level: {val}") 102 | 103 | 104 | def test_account_margin_stopout_level(account: Account): 105 | val = account.margin_stopout_level 106 | assert isinstance(val, float) 107 | print(f"Account margin_stopout_level: {val}") 108 | 109 | 110 | def test_account_name(account: Account): 111 | val = account.name 112 | assert isinstance(val, str) 113 | print(f"Account name: {val}") 114 | 115 | 116 | def test_account_server(account: Account): 117 | val = account.server 118 | assert isinstance(val, str) 119 | print(f"Account server: {val}") 120 | 121 | 122 | def test_account_currency(account: Account): 123 | val = account.currency 124 | assert isinstance(val, str) 125 | print(f"Account currency: {val}") 126 | 127 | 128 | def test_account_company(account: Account): 129 | val = account.company 130 | assert isinstance(val, str) 131 | print(f"Account company: {val}") 132 | -------------------------------------------------------------------------------- /mt4client/api/order.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class OrderType(Enum): 5 | """The order types available in MetaTrader 4. 6 | 7 | Reference: 8 | https://docs.mql4.com/trading/ordertype 9 | 10 | https://www.mql5.com/en/forum/122847 11 | """ 12 | 13 | OP_BUY = 0 14 | OP_SELL = 1 15 | OP_BUYLIMIT = 2 16 | OP_BUYSTOP = 3 17 | OP_SELLLIMIT = 4 18 | OP_SELLSTOP = 5 19 | # the following types are not for making orders; they represent special broker actions shown in Account History 20 | OP_BALANCE = 6 21 | OP_CREDIT = 7 22 | OP_REBATE = 8 23 | 24 | @property 25 | def is_buy(self) -> bool: 26 | """ 27 | :return: Whether the order type is a buy offer. 28 | """ 29 | return self == OrderType.OP_BUY or self == OrderType.OP_BUYLIMIT or self == OrderType.OP_BUYSTOP 30 | 31 | @property 32 | def is_sell(self) -> bool: 33 | """ 34 | :return: Whether the order type is a sell offer. 35 | """ 36 | return self == OrderType.OP_SELL or self == OrderType.OP_SELLLIMIT or self == OrderType.OP_SELLSTOP 37 | 38 | @property 39 | def is_market(self) -> bool: 40 | """ 41 | :return: Whether the order type is market. 42 | """ 43 | return self == OrderType.OP_BUY or self == OrderType.OP_SELL 44 | 45 | @property 46 | def is_pending(self) -> bool: 47 | """ 48 | :return: Whether the order type is pending. 49 | """ 50 | return (self == OrderType.OP_BUYLIMIT or self == OrderType.OP_BUYSTOP or 51 | self == OrderType.OP_SELLLIMIT or self == OrderType.OP_SELLSTOP) 52 | 53 | def __str__(self) -> str: 54 | if self is OrderType.OP_BUY: 55 | return "MARKET-BUY" 56 | elif self is OrderType.OP_SELL: 57 | return "MARKET-SELL" 58 | elif self is OrderType.OP_BUYLIMIT: 59 | return "LIMIT-BUY" 60 | elif self is OrderType.OP_BUYSTOP: 61 | return "STOP-BUY" 62 | elif self is OrderType.OP_SELLLIMIT: 63 | return "LIMIT-SELL" 64 | elif self is OrderType.OP_SELLSTOP: 65 | return "STOP-SELL" 66 | elif self is OrderType.OP_BALANCE: 67 | return "BALANCE" 68 | elif self is OrderType.OP_CREDIT: 69 | return "CREDIT" 70 | elif self is OrderType.OP_REBATE: 71 | return "REBATE" 72 | else: 73 | return "UNKNOWN" 74 | 75 | 76 | class Order: 77 | """Represents an order in MetaTrader 4. 78 | 79 | Reference: 80 | https://docs.mql4.com/trading 81 | """ 82 | 83 | def __init__(self, ticket: int, magic_number: int, symbol: str, order_type: int, lots: float, 84 | open_price: float, close_price: float, open_time: str, close_time: str, expiration: str, 85 | sl: float, tp: float, profit: float, commission: float, swap: float, comment: str): 86 | self.ticket = ticket 87 | """The order ticket number.""" 88 | 89 | self.magic_number = magic_number 90 | """The identifying (magic) number.""" 91 | 92 | self.symbol = symbol 93 | """The symbol name.""" 94 | 95 | self.order_type: OrderType = OrderType(order_type) 96 | """The order type.""" 97 | 98 | self.lots = lots 99 | """Amount of lots (trade volume).""" 100 | 101 | self.open_price = open_price 102 | """The open price.""" 103 | 104 | self.close_price = close_price 105 | """The close price.""" 106 | 107 | self.open_time = open_time 108 | """The open date/time.""" 109 | 110 | self.close_time = close_time 111 | """The close date/time.""" 112 | 113 | self.expiration = expiration 114 | """The expiration date/time.""" 115 | 116 | self.sl = sl 117 | """The stop-loss.""" 118 | 119 | self.tp = tp 120 | """The take-profit.""" 121 | 122 | self.profit = profit 123 | """The net profit (without swaps or commissions). For open orders, it is the current 124 | unrealized profit. For closed orders, it is the fixed profit. 125 | """ 126 | 127 | self.commission = commission 128 | """The calculated commission.""" 129 | 130 | self.swap = swap 131 | """The swap value.""" 132 | 133 | self.comment = comment 134 | """The comment.""" 135 | 136 | def __repr__(self): 137 | return (f"{self.__class__.__name__}(" 138 | f"ticket={self.ticket}, " 139 | f"magic_number={self.magic_number}, " 140 | f"symbol={self.symbol}, " 141 | f"order_type={self.order_type}, " 142 | f"lots={self.lots}, " 143 | f"open_price={self.open_price}, " 144 | f"close_price={self.close_price}, " 145 | f"open_time={self.open_time}, " 146 | f"close_time={self.close_time}, " 147 | f"expiration={self.expiration}, " 148 | f"sl={self.sl}, " 149 | f"tp={self.tp}, " 150 | f"profit={self.profit}, " 151 | f"commission={self.commission}, " 152 | f"swap={self.swap}, " 153 | f"comment={self.comment})") 154 | -------------------------------------------------------------------------------- /tests/test_order.py: -------------------------------------------------------------------------------- 1 | from mt4client import MT4Client 2 | from mt4client.api import Symbol, Order, OrderType 3 | 4 | 5 | def test_market_buy(mt4: MT4Client, symbol: Symbol) -> Order: 6 | # create a market order using relative stops 7 | bid = symbol.tick.bid 8 | points = 50 9 | order = mt4.order_send( 10 | symbol=symbol, 11 | lots=symbol.volume_min, 12 | order_type=OrderType.OP_BUY, 13 | sl_points=points, 14 | tp_points=points) 15 | 16 | assert isinstance(order, Order) 17 | assert order.order_type == OrderType.OP_BUY 18 | assert order.sl < bid 19 | assert order.tp > bid 20 | print(f"Order successful: {order}") 21 | return order 22 | 23 | 24 | def test_market_sell(mt4: MT4Client, symbol: Symbol) -> Order: 25 | # create a market order using absolute stops 26 | bid = symbol.tick.bid 27 | points = 100 28 | sl = bid + points * symbol.point 29 | tp = bid - points * symbol.point 30 | order = mt4.order_send( 31 | symbol=symbol, 32 | lots=symbol.volume_min, 33 | order_type=OrderType.OP_SELL, 34 | sl=sl, 35 | tp=tp 36 | ) 37 | 38 | assert isinstance(order, Order) 39 | assert order.order_type == OrderType.OP_SELL 40 | assert order.sl > bid 41 | assert order.tp < bid 42 | print(f"Order successful: {order}") 43 | return order 44 | 45 | 46 | def test_limit_buy(mt4: MT4Client, symbol: Symbol) -> Order: 47 | # create a pending buy order with relative sl/tp 48 | optimistic_buy_price = symbol.tick.ask / 2 49 | slippage = 1 50 | sl_points = 100 51 | tp_points = 100 52 | order = mt4.order_send( 53 | symbol=symbol, 54 | lots=symbol.volume_min, 55 | order_type=OrderType.OP_BUYLIMIT, 56 | price=optimistic_buy_price, 57 | slippage=slippage, 58 | sl_points=sl_points, 59 | tp_points=tp_points 60 | ) 61 | 62 | assert isinstance(order, Order) 63 | assert order.order_type == OrderType.OP_BUYLIMIT 64 | print(f"Order successful: {order}") 65 | return order 66 | 67 | 68 | def test_limit_sell(mt4: MT4Client, symbol: Symbol) -> Order: 69 | # create a pending sell order with no sl/tp 70 | optimistic_sell_price = symbol.tick.bid * 2 71 | price = optimistic_sell_price 72 | slippage = 1 73 | order = mt4.order_send( 74 | symbol=symbol, 75 | lots=symbol.volume_min, 76 | order_type=OrderType.OP_SELLLIMIT, 77 | price=price, 78 | slippage=slippage 79 | ) 80 | 81 | assert isinstance(order, Order) 82 | assert order.order_type == OrderType.OP_SELLLIMIT 83 | print(f"Order successful: {order}") 84 | return order 85 | 86 | 87 | def test_modify_open_order(mt4: MT4Client, symbol: Symbol): 88 | # create a market order 89 | order = mt4.order_send( 90 | symbol=symbol, 91 | lots=symbol.volume_min, 92 | order_type=OrderType.OP_BUY 93 | ) 94 | 95 | # add sl/tp stops 96 | bid = symbol.tick.bid 97 | points = 200 98 | sl = bid - points * symbol.point 99 | tp = bid + points * symbol.point 100 | 101 | # modify order 102 | order = mt4.order_modify(order=order, sl=sl, tp=tp) 103 | assert order.sl < bid 104 | assert order.tp > bid 105 | print(f"Order open_price/sl/tp: {order.open_price}/{order.sl}/{order.tp}") 106 | 107 | 108 | def test_modify_pending_order(mt4: MT4Client, symbol: Symbol): 109 | # create a pending order 110 | optimistic_buy_price = symbol.tick.ask / 2 111 | slippage = 1 112 | sl_points = 100 113 | tp_points = 100 114 | order = mt4.order_send( 115 | symbol=symbol, 116 | lots=symbol.volume_min, 117 | order_type=OrderType.OP_BUYLIMIT, 118 | price=optimistic_buy_price, 119 | slippage=slippage, 120 | sl_points=sl_points, 121 | tp_points=tp_points 122 | ) 123 | original_price = order.open_price 124 | 125 | # lower the price and widen the sl/tp windows 126 | new_price = original_price * 0.9 127 | new_points = 100 128 | 129 | # modify order 130 | order = mt4.order_modify(order=order, price=new_price, sl_points=new_points, tp_points=new_points) 131 | assert order.open_price < original_price 132 | assert order.sl < new_price 133 | assert order.tp > new_price 134 | print(f"Order open_price/sl/tp: {order.open_price}/{order.sl}/{order.tp}") 135 | 136 | 137 | def test_close_open_order(mt4: MT4Client, symbol: Symbol): 138 | # create a market order 139 | order = mt4.order_send( 140 | symbol=symbol, 141 | lots=symbol.volume_min, 142 | order_type=OrderType.OP_BUY 143 | ) 144 | 145 | # assert that the order was created and is open 146 | assert order is not None 147 | assert order.order_type.is_market 148 | 149 | # close the order 150 | mt4.order_close(order) 151 | search_results = [x for x in mt4.orders() if x.ticket == order.ticket] 152 | assert len(search_results) == 0 153 | print(f"Open order # {order.ticket} was closed.") 154 | 155 | 156 | def test_delete_pending_order(mt4: MT4Client, symbol: Symbol): 157 | # create a pending order 158 | optimistic_buy_price = symbol.tick.ask / 2 159 | order = mt4.order_send( 160 | symbol=symbol, 161 | lots=symbol.volume_min, 162 | order_type=OrderType.OP_BUYLIMIT, 163 | price=optimistic_buy_price 164 | ) 165 | 166 | # assert that the order was created and is pending 167 | assert order is not None 168 | assert order.order_type.is_pending 169 | 170 | # delete the order 171 | mt4.order_delete(order) 172 | search_results = [x for x in mt4.orders() if x.ticket == order.ticket] 173 | assert len(search_results) == 0 174 | print(f"Pending order # {order.ticket} was deleted.") 175 | 176 | 177 | def test_orders(mt4: MT4Client): 178 | orders = mt4.orders() 179 | assert isinstance(orders, list) 180 | print(f"Found {len(orders)} orders.") 181 | 182 | 183 | def test_orders_historical(mt4: MT4Client): 184 | orders = mt4.orders_historical() 185 | assert isinstance(orders, list) 186 | print(f"Found {len(orders)} historical orders.") 187 | 188 | 189 | def test_close_all_orders(mt4: MT4Client): 190 | # close/delete all orders 191 | for order in mt4.orders(): 192 | mt4.order_delete(order, close_if_opened=True) 193 | assert len(mt4.orders()) == 0 194 | -------------------------------------------------------------------------------- /mt4client/api/account.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from enum import Enum 3 | from typing import TYPE_CHECKING 4 | if TYPE_CHECKING: 5 | from mt4client.client import MT4Client 6 | 7 | 8 | class AccountInfoInteger(Enum): 9 | """ 10 | MetaTrader 4 account properties which return an integer. 11 | 12 | References: 13 | https://docs.mql4.com/constants/environment_state/accountinformation#enum_account_info_integer 14 | """ 15 | ACCOUNT_LOGIN = 0 16 | ACCOUNT_TRADE_MODE = 32 17 | ACCOUNT_LEVERAGE = 35 18 | ACCOUNT_LIMIT_ORDERS = 47 19 | ACCOUNT_MARGIN_SO_MODE = 44 20 | ACCOUNT_TRADE_ALLOWED = 33 21 | ACCOUNT_TRADE_EXPERT = 34 22 | 23 | 24 | class AccountInfoDouble(Enum): 25 | """MetaTrader 4 account properties which return a double. 26 | 27 | References: 28 | https://docs.mql4.com/constants/environment_state/accountinformation#enum_account_info_double 29 | """ 30 | ACCOUNT_BALANCE = 37 31 | ACCOUNT_CREDIT = 38 32 | ACCOUNT_PROFIT = 39 33 | ACCOUNT_EQUITY = 40 34 | ACCOUNT_MARGIN = 41 35 | ACCOUNT_MARGIN_FREE = 42 36 | ACCOUNT_MARGIN_LEVEL = 43 37 | ACCOUNT_MARGIN_SO_CALL = 45 38 | ACCOUNT_MARGIN_SO_SO = 46 39 | ACCOUNT_MARGIN_INITIAL = 48 # deprecated 40 | ACCOUNT_MARGIN_MAINTENANCE = 49 # deprecated 41 | ACCOUNT_ASSETS = 50 # deprecated 42 | ACCOUNT_LIABILITIES = 51 # deprecated 43 | ACCOUNT_COMMISSION_BLOCKED = 52 # deprecated 44 | 45 | 46 | class AccountStopoutMode(Enum): 47 | """ 48 | MetaTrader 4 account's stop out mode. 49 | 50 | References: 51 | https://docs.mql4.com/constants/environment_state/accountinformation#enum_account_stopout_mode 52 | """ 53 | ACCOUNT_STOPOUT_MODE_PERCENT = 0 54 | ACCOUNT_STOPOUT_MODE_MONEY = 1 55 | 56 | 57 | class AccountTradeMode(Enum): 58 | """ 59 | MetaTrader 4 account type. 60 | 61 | References: 62 | https://docs.mql4.com/constants/environment_state/accountinformation#enum_account_trade_mode 63 | """ 64 | ACCOUNT_TRADE_MODE_DEMO = 0 65 | ACCOUNT_TRADE_MODE_CONTEST = 1 66 | ACCOUNT_TRADE_MODE_REAL = 2 67 | 68 | 69 | class Account: 70 | """A MetaTrader 4 account.""" 71 | 72 | def __init__(self, mt4: MT4Client, login: int, trade_mode: int, name: str, server: str, currency: str, 73 | company: str): 74 | self._mt4 = mt4 75 | 76 | self.login = login 77 | """The account number.""" 78 | 79 | self.trade_mode: AccountTradeMode = AccountTradeMode(trade_mode) 80 | """Account trade mode.""" 81 | 82 | self.name = name 83 | """Client name.""" 84 | 85 | self.server = server 86 | """Trade server name.""" 87 | 88 | self.currency = currency 89 | """Account currency.""" 90 | 91 | self.company = company 92 | """Name of a company that serves the account.""" 93 | 94 | @property 95 | def leverage(self) -> int: 96 | """Account leverage.""" 97 | return self._get_account_info_integer(AccountInfoInteger.ACCOUNT_LEVERAGE) 98 | 99 | @property 100 | def limit_orders(self) -> int: 101 | """Maximum allowed number of open positions and active pending orders (in total), 0 = unlimited.""" 102 | return self._get_account_info_integer(AccountInfoInteger.ACCOUNT_LIMIT_ORDERS) 103 | 104 | @property 105 | def margin_so_mode(self) -> AccountStopoutMode: 106 | """Mode for setting the minimal allowed margin.""" 107 | val = self._get_account_info_integer(AccountInfoInteger.ACCOUNT_MARGIN_SO_MODE) 108 | return AccountStopoutMode(val) 109 | 110 | @property 111 | def trade_allowed(self) -> bool: 112 | """Allowed trade for the current account.""" 113 | val = self._get_account_info_integer(AccountInfoInteger.ACCOUNT_TRADE_ALLOWED) 114 | return bool(val) 115 | 116 | @property 117 | def trade_expert(self) -> int: 118 | """Allowed trade for an Expert Advisor.""" 119 | val = self._get_account_info_integer(AccountInfoInteger.ACCOUNT_TRADE_EXPERT) 120 | return bool(val) 121 | 122 | @property 123 | def balance(self) -> float: 124 | """Account balance in the deposit currency.""" 125 | return self._get_account_info_double(AccountInfoDouble.ACCOUNT_BALANCE) 126 | 127 | @property 128 | def credit(self) -> float: 129 | """Account credit in the deposit currency.""" 130 | return self._get_account_info_double(AccountInfoDouble.ACCOUNT_CREDIT) 131 | 132 | @property 133 | def profit(self) -> float: 134 | """Current profit of an account in the deposit currency.""" 135 | return self._get_account_info_double(AccountInfoDouble.ACCOUNT_PROFIT) 136 | 137 | @property 138 | def equity(self) -> float: 139 | """Account equity in the deposit currency.""" 140 | return self._get_account_info_double(AccountInfoDouble.ACCOUNT_EQUITY) 141 | 142 | @property 143 | def margin(self) -> float: 144 | """Account margin used in the deposit currency.""" 145 | return self._get_account_info_double(AccountInfoDouble.ACCOUNT_MARGIN) 146 | 147 | @property 148 | def margin_free(self) -> float: 149 | """Free margin of an account in the deposit currency.""" 150 | return self._get_account_info_double(AccountInfoDouble.ACCOUNT_MARGIN_FREE) 151 | 152 | @property 153 | def margin_level(self) -> float: 154 | """Account margin level in percents.""" 155 | return self._get_account_info_double(AccountInfoDouble.ACCOUNT_MARGIN_LEVEL) 156 | 157 | @property 158 | def margin_call_level(self) -> float: 159 | """Margin call level. Depending on :attr:`margin_so_mode`, this is expressed in percents or in 160 | the deposit currency. 161 | """ 162 | return self._get_account_info_double(AccountInfoDouble.ACCOUNT_MARGIN_SO_CALL) 163 | 164 | @property 165 | def margin_stopout_level(self) -> float: 166 | """Margin stop out level. Depending on the :attr:`margin_so_mode`, this is expressed in percents or in 167 | the deposit currency. 168 | """ 169 | return self._get_account_info_double(AccountInfoDouble.ACCOUNT_MARGIN_SO_SO) 170 | 171 | def _get_account_info_integer(self, prop: AccountInfoInteger) -> int: 172 | return self._mt4._get_response(request={ 173 | "action": "GET_ACCOUNT_INFO_INTEGER", 174 | "property_id": prop.value 175 | }) 176 | 177 | def _get_account_info_double(self, prop: AccountInfoDouble) -> float: 178 | return self._mt4._get_response(request={ 179 | "action": "GET_ACCOUNT_INFO_DOUBLE", 180 | "property_id": prop.value 181 | }) 182 | 183 | def __repr__(self): 184 | return (f'{self.__class__.__name__}(' 185 | f'login={self.login}, ' 186 | f'trade_mode={self.trade_mode}, ' 187 | f'name={self.name}, ' 188 | f'server={self.server}, ' 189 | f'currency={self.currency}, ' 190 | f'company={self.company})') 191 | -------------------------------------------------------------------------------- /mt4client/api/errors.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from enum import Enum 3 | 4 | 5 | class MT4ErrorCode(Enum): 6 | """A MetaTrader 4 error code. 7 | 8 | Reference: 9 | https://docs.mql4.com/constants/errorswarnings/errorcodes 10 | """ 11 | UNKNOWN = -1 12 | ERR_NO_ERROR = 0 13 | ERR_NO_RESULT = 1 14 | ERR_COMMON_ERROR = 2 15 | ERR_INVALID_TRADE_PARAMETERS = 3 16 | ERR_SERVER_BUSY = 4 17 | ERR_OLD_VERSION = 5 18 | ERR_NO_CONNECTION = 6 19 | ERR_NOT_ENOUGH_RIGHTS = 7 20 | ERR_TOO_FREQUENT_REQUESTS = 8 21 | ERR_MALFUNCTIONAL_TRADE = 9 22 | ERR_ACCOUNT_DISABLED = 64 23 | ERR_INVALID_ACCOUNT = 65 24 | ERR_TRADE_TIMEOUT = 128 25 | ERR_INVALID_PRICE = 129 26 | ERR_INVALID_STOPS = 130 27 | ERR_INVALID_TRADE_VOLUME = 131 28 | ERR_MARKET_CLOSED = 132 29 | ERR_TRADE_DISABLED = 133 30 | ERR_NOT_ENOUGH_MONEY = 134 31 | ERR_PRICE_CHANGED = 135 32 | ERR_OFF_QUOTES = 136 33 | ERR_BROKER_BUSY = 137 34 | ERR_REQUOTE = 138 35 | ERR_ORDER_LOCKED = 139 36 | ERR_LONG_POSITIONS_ONLY_ALLOWED = 140 37 | ERR_TOO_MANY_REQUESTS = 141 38 | ERR_TRADE_MODIFY_DENIED = 145 39 | ERR_TRADE_CONTEXT_BUSY = 146 40 | ERR_TRADE_EXPIRATION_DENIED = 147 41 | ERR_TRADE_TOO_MANY_ORDERS = 148 42 | ERR_TRADE_HEDGE_PROHIBITED = 149 43 | ERR_TRADE_PROHIBITED_BY_FIFO = 150 44 | ERR_NO_MQLERROR = 4000 45 | ERR_WRONG_FUNCTION_POINTER = 4001 46 | ERR_ARRAY_INDEX_OUT_OF_RANGE = 4002 47 | ERR_NO_MEMORY_FOR_CALL_STACK = 4003 48 | ERR_RECURSIVE_STACK_OVERFLOW = 4004 49 | ERR_NOT_ENOUGH_STACK_FOR_PARAM = 4005 50 | ERR_NO_MEMORY_FOR_PARAM_STRING = 4006 51 | ERR_NO_MEMORY_FOR_TEMP_STRING = 4007 52 | ERR_NOT_INITIALIZED_STRING = 4008 53 | ERR_NOT_INITIALIZED_ARRAYSTRING = 4009 54 | ERR_NO_MEMORY_FOR_ARRAYSTRING = 4010 55 | ERR_TOO_LONG_STRING = 4011 56 | ERR_REMAINDER_FROM_ZERO_DIVIDE = 4012 57 | ERR_ZERO_DIVIDE = 4013 58 | ERR_UNKNOWN_COMMAND = 4014 59 | ERR_WRONG_JUMP = 4015 60 | ERR_NOT_INITIALIZED_ARRAY = 4016 61 | ERR_DLL_CALLS_NOT_ALLOWED = 4017 62 | ERR_CANNOT_LOAD_LIBRARY = 4018 63 | ERR_CANNOT_CALL_FUNCTION = 4019 64 | ERR_EXTERNAL_CALLS_NOT_ALLOWED = 4020 65 | ERR_NO_MEMORY_FOR_RETURNED_STR = 4021 66 | ERR_SYSTEM_BUSY = 4022 67 | ERR_DLLFUNC_CRITICALERROR = 4023 68 | ERR_INTERNAL_ERROR = 4024 69 | ERR_OUT_OF_MEMORY = 4025 70 | ERR_INVALID_POINTER = 4026 71 | ERR_FORMAT_TOO_MANY_FORMATTERS = 4027 72 | ERR_FORMAT_TOO_MANY_PARAMETERS = 4028 73 | ERR_ARRAY_INVALID = 4029 74 | ERR_CHART_NOREPLY = 4030 75 | ERR_INVALID_FUNCTION_PARAMSCNT = 4050 76 | ERR_INVALID_FUNCTION_PARAMVALUE = 4051 77 | ERR_STRING_FUNCTION_INTERNAL = 4052 78 | ERR_SOME_ARRAY_ERROR = 4053 79 | ERR_INCORRECT_SERIESARRAY_USING = 4054 80 | ERR_CUSTOM_INDICATOR_ERROR = 4055 81 | ERR_INCOMPATIBLE_ARRAYS = 4056 82 | ERR_GLOBAL_VARIABLES_PROCESSING = 4057 83 | ERR_GLOBAL_VARIABLE_NOT_FOUND = 4058 84 | ERR_FUNC_NOT_ALLOWED_IN_TESTING = 4059 85 | ERR_FUNCTION_NOT_CONFIRMED = 4060 86 | ERR_SEND_MAIL_ERROR = 4061 87 | ERR_STRING_PARAMETER_EXPECTED = 4062 88 | ERR_INTEGER_PARAMETER_EXPECTED = 4063 89 | ERR_DOUBLE_PARAMETER_EXPECTED = 4064 90 | ERR_ARRAY_AS_PARAMETER_EXPECTED = 4065 91 | ERR_HISTORY_WILL_UPDATED = 4066 92 | ERR_TRADE_ERROR = 4067 93 | ERR_RESOURCE_NOT_FOUND = 4068 94 | ERR_RESOURCE_NOT_SUPPORTED = 4069 95 | ERR_RESOURCE_DUPLICATED = 4070 96 | ERR_INDICATOR_CANNOT_INIT = 4071 97 | ERR_INDICATOR_CANNOT_LOAD = 4072 98 | ERR_NO_HISTORY_DATA = 4073 99 | ERR_NO_MEMORY_FOR_HISTORY = 4074 100 | ERR_NO_MEMORY_FOR_INDICATOR = 4075 101 | ERR_END_OF_FILE = 4099 102 | ERR_SOME_FILE_ERROR = 4100 103 | ERR_WRONG_FILE_NAME = 4101 104 | ERR_TOO_MANY_OPENED_FILES = 4102 105 | ERR_CANNOT_OPEN_FILE = 4103 106 | ERR_INCOMPATIBLE_FILEACCESS = 4104 107 | ERR_NO_ORDER_SELECTED = 4105 108 | ERR_UNKNOWN_SYMBOL = 4106 109 | ERR_INVALID_PRICE_PARAM = 4107 110 | ERR_INVALID_TICKET = 4108 111 | ERR_TRADE_NOT_ALLOWED = 4109 112 | ERR_LONGS_NOT_ALLOWED = 4110 113 | ERR_SHORTS_NOT_ALLOWED = 4111 114 | ERR_TRADE_EXPERT_DISABLED_BY_SERVER = 4112 115 | ERR_OBJECT_ALREADY_EXISTS = 4200 116 | ERR_UNKNOWN_OBJECT_PROPERTY = 4201 117 | ERR_OBJECT_DOES_NOT_EXIST = 4202 118 | ERR_UNKNOWN_OBJECT_TYPE = 4203 119 | ERR_NO_OBJECT_NAME = 4204 120 | ERR_OBJECT_COORDINATES_ERROR = 4205 121 | ERR_NO_SPECIFIED_SUBWINDOW = 4206 122 | ERR_SOME_OBJECT_ERROR = 4207 123 | ERR_CHART_PROP_INVALID = 4210 124 | ERR_CHART_NOT_FOUND = 4211 125 | ERR_CHARTWINDOW_NOT_FOUND = 4212 126 | ERR_CHARTINDICATOR_NOT_FOUND = 4213 127 | ERR_SYMBOL_SELECT = 4220 128 | ERR_NOTIFICATION_ERROR = 4250 129 | ERR_NOTIFICATION_PARAMETER = 4251 130 | ERR_NOTIFICATION_SETTINGS = 4252 131 | ERR_NOTIFICATION_TOO_FREQUENT = 4253 132 | ERR_FTP_NOSERVER = 4260 133 | ERR_FTP_NOLOGIN = 4261 134 | ERR_FTP_CONNECT_FAILED = 4262 135 | ERR_FTP_CLOSED = 4263 136 | ERR_FTP_CHANGEDIR = 4264 137 | ERR_FTP_FILE_ERROR = 4265 138 | ERR_FTP_ERROR = 4266 139 | ERR_FILE_TOO_MANY_OPENED = 5001 140 | ERR_FILE_WRONG_FILENAME = 5002 141 | ERR_FILE_TOO_LONG_FILENAME = 5003 142 | ERR_FILE_CANNOT_OPEN = 5004 143 | ERR_FILE_BUFFER_ALLOCATION_ERROR = 5005 144 | ERR_FILE_CANNOT_DELETE = 5006 145 | ERR_FILE_INVALID_HANDLE = 5007 146 | ERR_FILE_WRONG_HANDLE = 5008 147 | ERR_FILE_NOT_TOWRITE = 5009 148 | ERR_FILE_NOT_TOREAD = 5010 149 | ERR_FILE_NOT_BIN = 5011 150 | ERR_FILE_NOT_TXT = 5012 151 | ERR_FILE_NOT_TXTORCSV = 5013 152 | ERR_FILE_NOT_CSV = 5014 153 | ERR_FILE_READ_ERROR = 5015 154 | ERR_FILE_WRITE_ERROR = 5016 155 | ERR_FILE_BIN_STRINGSIZE = 5017 156 | ERR_FILE_INCOMPATIBLE = 5018 157 | ERR_FILE_IS_DIRECTORY = 5019 158 | ERR_FILE_NOT_EXIST = 5020 159 | ERR_FILE_CANNOT_REWRITE = 5021 160 | ERR_FILE_WRONG_DIRECTORYNAME = 5022 161 | ERR_FILE_DIRECTORY_NOT_EXIST = 5023 162 | ERR_FILE_NOT_DIRECTORY = 5024 163 | ERR_FILE_CANNOT_DELETE_DIRECTORY = 5025 164 | ERR_FILE_CANNOT_CLEAN_DIRECTORY = 5026 165 | ERR_FILE_ARRAYRESIZE_ERROR = 5027 166 | ERR_FILE_STRINGRESIZE_ERROR = 5028 167 | ERR_FILE_STRUCT_WITH_OBJECTS = 5029 168 | ERR_WEBREQUEST_INVALID_ADDRESS = 5200 169 | ERR_WEBREQUEST_CONNECT_FAILED = 5201 170 | ERR_WEBREQUEST_TIMEOUT = 5202 171 | ERR_WEBREQUEST_REQUEST_FAILED = 5203 172 | ERR_USER_ERROR_FIRST = 65536 173 | 174 | @staticmethod 175 | def parse(code: int) -> MT4ErrorCode: 176 | try: 177 | return MT4ErrorCode(code) if code is not None else MT4ErrorCode.UNKNOWN 178 | except ValueError: 179 | return MT4ErrorCode.UNKNOWN 180 | 181 | 182 | class MT4Error(Exception): 183 | """Represents an error from the MT4 terminal/server.""" 184 | 185 | def __init__(self, error_code: int = None, error_code_description: str = None, message: str = None): 186 | """ 187 | :param error_code: The error code. 188 | :param error_code_description: The error code description. 189 | :param message: Additional details about the error. 190 | """ 191 | self.error_code = MT4ErrorCode.parse(error_code) 192 | self.error_code_description = error_code_description 193 | self.message = message 194 | 195 | def __str__(self): 196 | if self.error_code_description is not None and self.message is not None: 197 | return "[MT4 Error {}: {}] {}\n {}".format(self.error_code.value, self.error_code.name, self.error_code_description, self.message) 198 | elif self.error_code_description is not None and self.message is None: 199 | return "[MT4 Error {}: {}] {}".format(self.error_code.value, self.error_code.name, self.error_code_description) 200 | elif self.message is not None: 201 | return "[MT4 Error {}: {}]\n {}".format(self.error_code.value, self.error_code.name, self.message) 202 | else: 203 | return "[MT4 Error {}: {}]".format(self.error_code.value, self.error_code.name) 204 | 205 | def __repr__(self): 206 | return (f'{self.__class__.__name__}(' 207 | f'error_code={self.error_code}, ' 208 | f'error_code_description={self.error_code_description}, ' 209 | f'message={self.message})') 210 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "77585ab0e04099d7fc01c94fd667d837b04fa9f2545cd8087a98f6750c9504b0" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8.3" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "atomicwrites": { 20 | "hashes": [ 21 | "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", 22 | "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" 23 | ], 24 | "markers": "sys_platform == 'win32'", 25 | "version": "==1.4.0" 26 | }, 27 | "attrs": { 28 | "hashes": [ 29 | "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", 30 | "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" 31 | ], 32 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 33 | "version": "==21.2.0" 34 | }, 35 | "colorama": { 36 | "hashes": [ 37 | "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", 38 | "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" 39 | ], 40 | "markers": "sys_platform == 'win32'", 41 | "version": "==0.4.4" 42 | }, 43 | "iniconfig": { 44 | "hashes": [ 45 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 46 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 47 | ], 48 | "version": "==1.1.1" 49 | }, 50 | "packaging": { 51 | "hashes": [ 52 | "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966", 53 | "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0" 54 | ], 55 | "markers": "python_version >= '3.6'", 56 | "version": "==21.2" 57 | }, 58 | "pluggy": { 59 | "hashes": [ 60 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 61 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 62 | ], 63 | "markers": "python_version >= '3.6'", 64 | "version": "==1.0.0" 65 | }, 66 | "py": { 67 | "hashes": [ 68 | "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", 69 | "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" 70 | ], 71 | "index": "pypi", 72 | "version": "==1.10.0" 73 | }, 74 | "pyparsing": { 75 | "hashes": [ 76 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 77 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 78 | ], 79 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 80 | "version": "==2.4.7" 81 | }, 82 | "pytest": { 83 | "hashes": [ 84 | "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", 85 | "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" 86 | ], 87 | "index": "pypi", 88 | "version": "==6.2.5" 89 | }, 90 | "pyzmq": { 91 | "hashes": [ 92 | "sha256:08c4e315a76ef26eb833511ebf3fa87d182152adf43dedee8d79f998a2162a0b", 93 | "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74", 94 | "sha256:1621e7a2af72cced1f6ec8ca8ca91d0f76ac236ab2e8828ac8fe909512d566cb", 95 | "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0", 96 | "sha256:2841997a0d85b998cbafecb4183caf51fd19c4357075dfd33eb7efea57e4c149", 97 | "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d", 98 | "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f", 99 | "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9", 100 | "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df", 101 | "sha256:468bd59a588e276961a918a3060948ae68f6ff5a7fa10bb2f9160c18fe341067", 102 | "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0", 103 | "sha256:53f4fd13976789ffafedd4d46f954c7bb01146121812b72b4ddca286034df966", 104 | "sha256:62bcade20813796c426409a3e7423862d50ff0639f5a2a95be4b85b09a618666", 105 | "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d", 106 | "sha256:6b217b8f9dfb6628f74b94bdaf9f7408708cb02167d644edca33f38746ca12dd", 107 | "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd", 108 | "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f", 109 | "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268", 110 | "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d", 111 | "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8", 112 | "sha256:80e043a89c6cadefd3a0712f8a1322038e819ebe9dbac7eca3bce1721bcb63bf", 113 | "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b", 114 | "sha256:8eddc033e716f8c91c6a2112f0a8ebc5e00532b4a6ae1eb0ccc48e027f9c671c", 115 | "sha256:902319cfe23366595d3fa769b5b751e6ee6750a0a64c5d9f757d624b2ac3519e", 116 | "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356", 117 | "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e", 118 | "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36", 119 | "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70", 120 | "sha256:badb868fff14cfd0e200eaa845887b1011146a7d26d579aaa7f966c203736b92", 121 | "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c", 122 | "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7", 123 | "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7", 124 | "sha256:c88fa7410e9fc471e0858638f403739ee869924dd8e4ae26748496466e27ac59", 125 | "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57", 126 | "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2", 127 | "sha256:d1b5d457acbadcf8b27561deeaa386b0217f47626b29672fa7bd31deb6e91e1b", 128 | "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2", 129 | "sha256:d6157793719de168b199194f6b6173f0ccd3bf3499e6870fac17086072e39115", 130 | "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a", 131 | "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364", 132 | "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b", 133 | "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea", 134 | "sha256:ea5a79e808baef98c48c884effce05c31a0698c1057de8fc1c688891043c1ce1", 135 | "sha256:f43b4a2e6218371dd4f41e547bd919ceeb6ebf4abf31a7a0669cd11cd91ea973", 136 | "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45", 137 | "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93", 138 | "sha256:f907c7359ce8bf7f7e63c82f75ad0223384105f5126f313400b7e8004d9b33c3" 139 | ], 140 | "index": "pypi", 141 | "version": "==22.3.0" 142 | }, 143 | "toml": { 144 | "hashes": [ 145 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 146 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 147 | ], 148 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 149 | "version": "==0.10.2" 150 | } 151 | }, 152 | "develop": {} 153 | } 154 | -------------------------------------------------------------------------------- /mt4client/client.py: -------------------------------------------------------------------------------- 1 | import zmq 2 | 3 | from typing import Any, Dict, List, Union 4 | from mt4client.api import Account, MT4Error, Signal, Symbol, Order, OrderType 5 | 6 | 7 | class MT4Client: 8 | """Client interface for the MetaTrader 4 Server.""" 9 | 10 | def __init__(self, address: str = "tcp://localhost:28282", request_timeout_ms: int = 10000, 11 | response_timeout_ms: int = 10000, verbose: bool = False): 12 | """ 13 | Constructor. Initialize the REQ socket and connect to the MT4 server. 14 | 15 | :param address: The address of the server's listening socket. 16 | :param request_timeout_ms: The number of milliseconds to wait for a request to be sent. 17 | :param response_timeout_ms: The number of milliseconds to wait for a response to be received. 18 | :param verbose: Whether to print trace messages. 19 | """ 20 | self._verbose = verbose 21 | 22 | # create and configure REQ socket 23 | self._context = zmq.Context() 24 | self._socket = self._context.socket(zmq.REQ) 25 | self._socket.setsockopt(zmq.SNDHWM, 1) 26 | self._socket.setsockopt(zmq.RCVHWM, 1) 27 | self._socket.setsockopt(zmq.SNDTIMEO, request_timeout_ms) 28 | self._socket.setsockopt(zmq.RCVTIMEO, response_timeout_ms) 29 | 30 | # connect to server 31 | self._socket.connect(address) 32 | 33 | def shutdown(self): 34 | """Close all sockets immediately and terminate the ZeroMQ context.""" 35 | self._print_trace("Disconnecting...") 36 | self._context.destroy(0) 37 | 38 | def account(self) -> Account: 39 | """ 40 | Get a query interface for the account details. 41 | 42 | :return: The account object. 43 | """ 44 | resp = self._get_response(request={"action": "GET_ACCOUNT_INFO"}) 45 | return Account(mt4=self, **resp) 46 | 47 | def symbol_names(self) -> List[str]: 48 | """ 49 | Get the names of market symbols supported by the broker. 50 | 51 | :return: A list of symbol names. 52 | """ 53 | return self._get_response(request={"action": "GET_SYMBOLS"}, default=[]) 54 | 55 | def symbols(self, *names: str) -> Dict[str, Symbol]: 56 | """ 57 | Get query interfaces for market symbols. 58 | 59 | :param names: The names of the symbols. 60 | :return: A name-to-symbol dict of symbol objects. 61 | """ 62 | resp = self._get_response(request={ 63 | "action": "GET_SYMBOL_INFO", 64 | "names": names 65 | }) 66 | return {name: Symbol(mt4=self, **resp[name]) for name in names} 67 | 68 | def symbol(self, name: str) -> Symbol: 69 | """ 70 | Get a query interface for a market symbol. 71 | 72 | :param name: The name of the symbol. 73 | :return: The symbol object. 74 | """ 75 | resp = self._get_response(request={ 76 | "action": "GET_SYMBOL_INFO", 77 | "names": [name] 78 | }) 79 | return Symbol(mt4=self, **resp[name]) 80 | 81 | def signal_names(self) -> List[str]: 82 | """ 83 | Get the names of all trading signals. 84 | 85 | :return: A list of names of the available signals. 86 | """ 87 | return self._get_response(request={"action": "GET_SIGNALS"}, default=[]) 88 | 89 | def signals(self, *names: str) -> Dict[str, Signal]: 90 | """ 91 | Get data for multiple trading signals. 92 | 93 | :param names: The names of the signals. 94 | :return: A name-to-signal dict of signal objects. 95 | """ 96 | resp = self._get_response(request={ 97 | "action": "GET_SIGNAL_INFO", 98 | "names": names 99 | }, default={}) 100 | return {name: Signal(**resp[name]) for name in names} 101 | 102 | def signal(self, name: str) -> Signal: 103 | """ 104 | Get data for a trading signal. 105 | 106 | :param name: The name of the signal. 107 | :return: The signal object. 108 | """ 109 | resp = self._get_response(request={ 110 | "action": "GET_SIGNAL_INFO", 111 | "names": [name] 112 | }, default={}) 113 | return Signal(**resp[name]) 114 | 115 | def indicator(self, func: str, args: List[Union[str, int, float]], timeout: int = 5000) -> float: 116 | """ 117 | Run a built-in indicator function. 118 | 119 | References: 120 | https://docs.mql4.com/indicators 121 | 122 | :param func: The name of the indicator function. 123 | :param args: A list of function arguments. 124 | :param timeout: The maximum milliseconds to wait for the symbol's chart data to load. 125 | :return: The numeric result. 126 | """ 127 | return self._get_response(request={ 128 | "action": "RUN_INDICATOR", 129 | "indicator": func, 130 | "argv": args, 131 | "timeout": timeout 132 | }) 133 | 134 | def orders(self) -> List[Order]: 135 | """ 136 | Get the pending and open orders from the Trades tab. 137 | 138 | :return: A list of open or pending Orders. 139 | """ 140 | resp = self._get_response(request={ 141 | "action": "GET_ORDERS" 142 | }, default=[]) 143 | return [Order(**order_dict) for order_dict in resp] 144 | 145 | def orders_historical(self) -> List[Order]: 146 | """ 147 | Get the deleted and closed orders from the Account History tab. 148 | 149 | :return: A list of closed Orders. 150 | """ 151 | resp = self._get_response(request={ 152 | "action": "GET_HISTORICAL_ORDERS" 153 | }, default=[]) 154 | return [Order(**order_dict) for order_dict in resp] 155 | 156 | def order(self, ticket: int) -> Order: 157 | """ 158 | Get an order by ticket number. May be pending, open, or closed. 159 | 160 | :param ticket: The ticket number. 161 | :return: The Order object. 162 | """ 163 | resp = self._get_response(request={ 164 | "action": "GET_ORDER", 165 | "ticket": ticket 166 | }) 167 | return Order(**resp) 168 | 169 | def order_send(self, symbol: Union[Symbol, str], order_type: OrderType, lots: float, price: float = None, 170 | slippage: int = None, sl: float = None, tp: float = None, sl_points: int = None, 171 | tp_points: int = None, comment: str = "") -> Order: 172 | """ 173 | Create a new order. 174 | 175 | References: 176 | https://docs.mql4.com/trading/ordersend 177 | 178 | https://book.mql4.com/appendix/limits 179 | 180 | :param symbol: The market symbol object or name. 181 | :param order_type: The market order type. 182 | :param lots: The number of lots to trade. 183 | :param price: The desired open price. Optional for market orders. 184 | :param slippage: The maximum price slippage, in points. Omit to use a permissive default (2.0 * spread). 185 | :param sl: The absolute stop-loss to use. Optional. 186 | :param tp: The absolute take-profit to use. Optional. 187 | :param sl_points: The relative stop-loss to use, in points. Optional. 188 | :param tp_points: The relative take-profit to use, in points. Optional. 189 | :param comment: The order comment text. Last part of the comment may be changed by server. Optional. 190 | :return: The new Order. 191 | """ 192 | if not order_type.is_buy and not order_type.is_sell: 193 | raise ValueError("Invalid order type: " + str(order_type)) 194 | if order_type.is_pending and price is None: 195 | raise ValueError("Pending orders must specify a price") 196 | resp = self._get_response(request={ 197 | "action": "DO_ORDER_SEND", 198 | "symbol": symbol.name if isinstance(symbol, Symbol) else symbol, 199 | "order_type": order_type.value, 200 | "lots": lots, 201 | "price": price, 202 | "slippage": slippage, 203 | "sl": sl, 204 | "tp": tp, 205 | "sl_points": sl_points, 206 | "tp_points": tp_points, 207 | "comment": comment 208 | }) 209 | return Order(**resp) 210 | 211 | def order_modify(self, order: Union[Order, int], price: float = None, sl: float = None, tp: float = None, 212 | sl_points: int = None, tp_points: int = None) -> Order: 213 | """ 214 | Modify a market or pending order. 215 | 216 | References: 217 | https://book.mql4.com/trading/ordermodify 218 | 219 | https://book.mql4.com/appendix/limits 220 | 221 | :param order: An order or its ticket number. 222 | :param price: The desired open price; applies to pending orders only. Optional. 223 | :param sl: The absolute stop-loss to use. Optional. 224 | :param tp: The absolute take-profit to use. Optional. 225 | :param sl_points: The relative stop-loss to use, in points. Optional. 226 | :param tp_points: The relative take-profit to use, in points. Optional. 227 | :return: The Order with the updated values. 228 | """ 229 | resp = self._get_response(request={ 230 | "action": "DO_ORDER_MODIFY", 231 | "ticket": order.ticket if isinstance(order, Order) else order, 232 | "price": price, 233 | "sl": sl, 234 | "tp": tp, 235 | "sl_points": sl_points, 236 | "tp_points": tp_points 237 | }) 238 | return Order(**resp) 239 | 240 | def order_close(self, order: Union[Order, int]): 241 | """ 242 | Close an open order. 243 | 244 | References: 245 | https://docs.mql4.com/trading/orderdelete 246 | 247 | https://book.mql4.com/appendix/limits 248 | 249 | :param order: The order to close or its ticket number. 250 | """ 251 | self._get_response(request={ 252 | "action": "DO_ORDER_CLOSE", 253 | "ticket": order.ticket if isinstance(order, Order) else order 254 | }) 255 | 256 | def order_delete(self, order: Union[Order, int], close_if_opened: bool = False): 257 | """ 258 | Delete a pending order. 259 | 260 | References: 261 | https://docs.mql4.com/trading/orderdelete 262 | 263 | https://book.mql4.com/appendix/limits 264 | 265 | :param order: The order to close or its ticket number. 266 | :param close_if_opened: If true and the order is open, it is closed at market price. If false and the order is 267 | open, an `ERR_INVALID_TICKET` error is raised. 268 | """ 269 | self._get_response(request={ 270 | "action": "DO_ORDER_DELETE", 271 | "close_if_opened": close_if_opened, 272 | "ticket": order.ticket if isinstance(order, Order) else order 273 | }) 274 | 275 | def _get_response(self, request: Dict[str, Any], default: Any = None) -> Any: 276 | """ 277 | Send a request object to the server and wait for a response. 278 | 279 | :param request: The request to send. Must have an `action` property. 280 | :param default: The default return value. 281 | :return: The server response or the default value if response is empty. 282 | :raises: zmq.ZMQError, MT4Error 283 | """ 284 | self._socket.send_json(request) 285 | self._print_trace(f"Request: {request}") 286 | resp = self._socket.recv_json() 287 | if resp is None: 288 | self._print_trace("Response is empty.") 289 | else: 290 | self._print_trace(f"Response: {resp}") 291 | 292 | # raise any errors 293 | error_code = resp.get("error_code") 294 | error_code_description = resp.get("error_code_description") 295 | error_message = resp.get("error_message") 296 | if not (error_code is None and error_code_description is None and error_message is None): 297 | raise MT4Error(error_code, error_code_description, error_message) 298 | 299 | # print any warnings to STDOUT 300 | warning_message = resp.get("warning") 301 | if warning_message is not None: 302 | print(str(warning_message)) 303 | 304 | # unwrap the response 305 | return resp.get("response", default) 306 | 307 | def _print_trace(self, message: str): 308 | if self._verbose: 309 | print(f"[MT4-ZMQ] {message}") 310 | -------------------------------------------------------------------------------- /mt4client/api/symbol.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | from typing import TYPE_CHECKING, Union, List 5 | from mt4client.api import StandardTimeframe, NonStandardTimeframe, parse_timeframe, OHLCV 6 | 7 | if TYPE_CHECKING: 8 | from mt4client.client import MT4Client 9 | 10 | 11 | class MarketInfo(Enum): 12 | """MetaTrader 4 market information identifiers, used with MarketInfo() function. 13 | 14 | References: 15 | https://docs.mql4.com/constants/environment_state/marketinfoconstants 16 | """ 17 | MODE_LOW = 1 18 | MODE_HIGH = 2 19 | MODE_TIME = 5 20 | MODE_BID = 9 21 | MODE_ASK = 10 22 | MODE_POINT = 11 23 | MODE_DIGITS = 12 24 | MODE_SPREAD = 13 25 | MODE_STOPLEVEL = 14 26 | MODE_LOTSIZE = 15 27 | MODE_TICKVALUE = 16 28 | MODE_TICKSIZE = 17 29 | MODE_SWAPLONG = 18 30 | MODE_SWAPSHORT = 19 31 | MODE_STARTING = 20 32 | MODE_EXPIRATION = 21 33 | MODE_TRADEALLOWED = 22 34 | MODE_MINLOT = 23 35 | MODE_LOTSTEP = 24 36 | MODE_MAXLOT = 25 37 | MODE_SWAPTYPE = 26 38 | MODE_PROFITCALCMODE = 27 39 | MODE_MARGINCALCMODE = 28 40 | MODE_MARGININIT = 29 41 | MODE_MARGINMAINTENANCE = 30 42 | MODE_MARGINHEDGED = 31 43 | MODE_MARGINREQUIRED = 32 44 | MODE_FREEZELEVEL = 33 45 | MODE_CLOSEBY_ALLOWED = 34 46 | 47 | 48 | class SymbolInfoInteger(Enum): 49 | """MetaTrader 4 market symbol properties which return an integer. 50 | 51 | References: 52 | https://docs.mql4.com/constants/environment_state/marketinfoconstants#enum_symbol_info_integer 53 | """ 54 | SYMBOL_SELECT = 0 55 | SYMBOL_VISIBLE = 76 56 | SYMBOL_SESSION_DEALS = 56 # MQL5 only 57 | SYMBOL_SESSION_BUY_ORDERS = 60 # MQL5 only 58 | SYMBOL_SESSION_SELL_ORDERS = 62 # MQL5 only 59 | SYMBOL_VOLUME = 10 # MQL5 only 60 | SYMBOL_VOLUMEHIGH = 11 # MQL5 only 61 | SYMBOL_VOLUMELOW = 12 # MQL5 only 62 | SYMBOL_TIME = 15 63 | SYMBOL_DIGITS = 17 64 | SYMBOL_SPREAD_FLOAT = 41 65 | SYMBOL_SPREAD = 18 66 | SYMBOL_TRADE_CALC_MODE = 29 67 | SYMBOL_TRADE_MODE = 30 68 | SYMBOL_START_TIME = 51 69 | SYMBOL_EXPIRATION_TIME = 52 70 | SYMBOL_TRADE_STOPS_LEVEL = 31 71 | SYMBOL_TRADE_FREEZE_LEVEL = 32 72 | SYMBOL_TRADE_EXEMODE = 33 73 | SYMBOL_SWAP_MODE = 37 74 | SYMBOL_SWAP_ROLLOVER3DAYS = 40 75 | SYMBOL_EXPIRATION_MODE = 49 # MQL5 only 76 | SYMBOL_FILLING_MODE = 50 # MQL5 only 77 | SYMBOL_ORDER_MODE = 71 # MQL5 only 78 | 79 | 80 | class SymbolInfoDouble(Enum): 81 | """MetaTrader 4 market symbol properties which return a double. 82 | 83 | References: 84 | https://docs.mql4.com/constants/environment_state/marketinfoconstants#enum_symbol_info_double 85 | """ 86 | SYMBOL_BID = 1 87 | SYMBOL_BIDHIGH = 2 # MQL 5 only 88 | SYMBOL_BIDLOW = 3 # MQL 5 only 89 | SYMBOL_ASK = 4 90 | SYMBOL_ASKHIGH = 5 # MQL 5 only 91 | SYMBOL_ASKLOW = 6 # MQL 5 only 92 | SYMBOL_LAST = 7 # MQL 5 only 93 | SYMBOL_LASTHIGH = 8 # MQL 5 only 94 | SYMBOL_LASTLOW = 9 # MQL 5 only 95 | SYMBOL_POINT = 16 96 | SYMBOL_TRADE_TICK_VALUE = 26 97 | SYMBOL_TRADE_TICK_VALUE_PROFIT = 53 # MQL 5 only 98 | SYMBOL_TRADE_TICK_VALUE_LOSS = 54 # MQL 5 only 99 | SYMBOL_TRADE_TICK_SIZE = 27 100 | SYMBOL_TRADE_CONTRACT_SIZE = 28 101 | SYMBOL_VOLUME_MIN = 34 102 | SYMBOL_VOLUME_MAX = 35 103 | SYMBOL_VOLUME_STEP = 36 104 | SYMBOL_VOLUME_LIMIT = 55 # MQL 5 only 105 | SYMBOL_SWAP_LONG = 38 106 | SYMBOL_SWAP_SHORT = 39 107 | SYMBOL_MARGIN_INITIAL = 42 108 | SYMBOL_MARGIN_MAINTENANCE = 43 109 | SYMBOL_MARGIN_LONG = 44 # MQL 5 only 110 | SYMBOL_MARGIN_SHORT = 45 # MQL 5 only 111 | SYMBOL_MARGIN_LIMIT = 46 # MQL 5 only 112 | SYMBOL_MARGIN_STOP = 47 # MQL 5 only 113 | SYMBOL_MARGIN_STOPLIMIT = 48 # MQL 5 only 114 | SYMBOL_SESSION_VOLUME = 57 # MQL 5 only 115 | SYMBOL_SESSION_TURNOVER = 58 # MQL 5 only 116 | SYMBOL_SESSION_INTEREST = 59 # MQL 5 only 117 | SYMBOL_SESSION_BUY_ORDERS_VOLUME = 61 # MQL 5 only 118 | SYMBOL_SESSION_SELL_ORDERS_VOLUME = 63 # MQL 5 only 119 | SYMBOL_SESSION_OPEN = 64 # MQL 5 only 120 | SYMBOL_SESSION_CLOSE = 65 # MQL 5 only 121 | SYMBOL_SESSION_AW = 66 # MQL 5 only 122 | SYMBOL_SESSION_PRICE_SETTLEMENT = 67 # MQL 5 only 123 | SYMBOL_SESSION_PRICE_LIMIT_MIN = 68 # MQL 5 only 124 | SYMBOL_SESSION_PRICE_LIMIT_MAX = 69 # MQL 5 only 125 | 126 | 127 | class SymbolCalcMode(Enum): 128 | """The contract price calculation mode. 129 | 130 | This is the MQL4 version; the MQL5 enum has more members and possibly different index values. 131 | 132 | References: 133 | https://www.mql5.com/en/docs/constants/environment_state/marketinfoconstants#enum_symbol_calc_mode 134 | https://docs.mql4.com/convert/stringformat 135 | """ 136 | SYMBOL_CALC_MODE_FOREX = 0 137 | SYMBOL_CALC_MODE_CFD = 1 138 | SYMBOL_CALC_MODE_FUTURES = 2 139 | SYMBOL_CALC_MODE_CFDINDEX = 3 140 | 141 | 142 | class SymbolTradeMode(Enum): 143 | """MetaTrader 4 market symbol trading mode. 144 | 145 | References: 146 | https://docs.mql4.com/constants/environment_state/marketinfoconstants#enum_symbol_trade_mode 147 | """ 148 | SYMBOL_TRADE_MODE_DISABLED = 0 149 | SYMBOL_TRADE_MODE_LONGONLY = 3 # MQL5 only 150 | SYMBOL_TRADE_MODE_SHORTONLY = 4 # MQL5 only 151 | SYMBOL_TRADE_MODE_CLOSEONLY = 1 152 | SYMBOL_TRADE_MODE_FULL = 2 153 | 154 | 155 | class SymbolTradeExecution(Enum): 156 | """MetaTrader 4 market symbol deal execution modes. 157 | 158 | References: 159 | https://docs.mql4.com/constants/environment_state/marketinfoconstants#enum_symbol_trade_execution 160 | """ 161 | SYMBOL_TRADE_EXECUTION_REQUEST = 0 162 | SYMBOL_TRADE_EXECUTION_INSTANT = 1 163 | SYMBOL_TRADE_EXECUTION_MARKET = 2 164 | SYMBOL_TRADE_EXECUTION_EXCHANGE = 3 # MQL5 only 165 | 166 | 167 | class SymbolSwapMode(Enum): 168 | """The method of swap calculation. 169 | 170 | This is the MQL4 version; the MQL5 enum has more members and possibly different index values. 171 | 172 | References: 173 | https://www.mql5.com/en/docs/constants/environment_state/marketinfoconstants#enum_symbol_swap_mode 174 | https://docs.mql4.com/convert/stringformat 175 | """ 176 | SYMBOL_SWAP_MODE_POINTS = 0 177 | SYMBOL_SWAP_MODE_CURRENCY_SYMBOL = 1 178 | SYMBOL_SWAP_MODE_INTEREST_CURRENT = 2 179 | SYMBOL_SWAP_MODE_CURRENCY_MARGIN = 3 180 | 181 | 182 | class DayOfWeek(Enum): 183 | """Days of the week. 184 | 185 | References: 186 | https://docs.mql4.com/constants/environment_state/marketinfoconstants#enum_day_of_week 187 | """ 188 | SUNDAY = 0 189 | MONDAY = 1 190 | TUESDAY = 2 191 | WEDNESDAY = 3 192 | THURSDAY = 4 193 | FRIDAY = 5 194 | SATURDAY = 6 195 | 196 | 197 | class SymbolTick: 198 | """The latest prices of a symbol in MetaTrader 4. 199 | 200 | Reference: 201 | https://docs.mql4.com/constants/structures/mqltick 202 | """ 203 | 204 | def __init__(self, time: int, bid: float, ask: float, last: float, volume: int): 205 | self.time = time 206 | """The time of the last prices update.""" 207 | 208 | self.bid = bid 209 | """The current bid price.""" 210 | 211 | self.ask = ask 212 | """The current ask price.""" 213 | 214 | self.last = last 215 | """The price of the last deal (Last).""" 216 | 217 | self.volume = volume 218 | """The volume for the current Last price.""" 219 | 220 | def __repr__(self): 221 | return (f"{self.__class__.__name__}(" 222 | f"time={self.time}, " 223 | f"bid={self.bid}, " 224 | f"ask={self.ask}, " 225 | f"last={self.last}, " 226 | f"volume={self.volume})") 227 | 228 | 229 | class Symbol: 230 | """A market symbol in MetaTrader 4. 231 | 232 | Reference: 233 | https://docs.mql4.com/constants/environment_state/marketinfoconstants 234 | """ 235 | 236 | def __init__(self, mt4: MT4Client, name: str, point: float, digits: int, volume_min: float, 237 | volume_step: float, volume_max: float, trade_contract_size: float, trade_tick_value: float, 238 | trade_tick_size: float, trade_stops_level: int, trade_freeze_level: int): 239 | self._mt4 = mt4 240 | 241 | self.name = name 242 | """Symbol name.""" 243 | 244 | self.point = point 245 | """Point size in the quote currency.""" 246 | 247 | self.digits = digits 248 | """Digits after the decimal point.""" 249 | 250 | self.volume_min = volume_min 251 | """Minimal volume for a deal.""" 252 | 253 | self.volume_step = volume_step 254 | """Minimal volume change step for deal execution.""" 255 | 256 | self.volume_max = volume_max 257 | """Maximal volume for a deal.""" 258 | 259 | self.trade_contract_size = trade_contract_size 260 | """Trade contract size in the base currency.""" 261 | 262 | self.trade_tick_value = trade_tick_value 263 | """Tick value in the deposit currency.""" 264 | 265 | self.trade_tick_size = trade_tick_size 266 | """Tick size in points.""" 267 | 268 | self.trade_stops_level = trade_stops_level 269 | """Stop level in points.""" 270 | 271 | self.trade_freeze_level = trade_freeze_level 272 | """Order freeze level in points.""" 273 | 274 | def ohlcv(self, timeframe: Union[str, StandardTimeframe, NonStandardTimeframe], limit: int = 100, 275 | timeout: int = 5000) -> List[OHLCV]: 276 | """ 277 | Fetches OHLCV data for this symbol, up to the current time. 278 | 279 | :param timeframe: The period. Use a standard timeframe for a higher likelihood of success. 280 | :param limit: The maximum number of bars to get. 281 | :param timeout: The maximum milliseconds to wait for the broker's server to provide the requested data. 282 | :return: A list of OHLCV bars, sorted oldest-to-newest, each having the following structure: 283 | [time, open, high, low, close, volume] 284 | """ 285 | period = parse_timeframe(timeframe).value if isinstance(timeframe, str) else timeframe.value 286 | bars = self._mt4._get_response(request={ 287 | "action": "GET_OHLCV", 288 | "symbol": self.name, 289 | "timeframe": period, 290 | "limit": limit, 291 | "timeout": timeout 292 | }, default=[]) 293 | return [OHLCV(**bar) for bar in bars] 294 | 295 | @property 296 | def tick(self) -> SymbolTick: 297 | """ 298 | The latest market prices of this symbol. 299 | 300 | References: 301 | https://docs.mql4.com/constants/structures/mqltick 302 | 303 | :return: The latest symbol tick. 304 | """ 305 | resp = self._mt4._get_response(request={ 306 | "action": "GET_SYMBOL_TICK", 307 | "symbol": self.name 308 | }) 309 | return SymbolTick(**resp) 310 | 311 | @property 312 | def is_selected(self) -> bool: 313 | """Whether symbol selected in Market Watch. 314 | 315 | Some symbols can be hidden in Market Watch, but still they are considered as selected. 316 | 317 | :return: `SymbolInfoInteger(:symbol, SYMBOL_SELECT)` 318 | """ 319 | return self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_SELECT) 320 | 321 | @property 322 | def is_visible(self) -> bool: 323 | """Whether symbol is visible in Market Watch. 324 | 325 | Some symbols (mostly, these are cross rates required for calculation of margin requirements or profits in 326 | deposit currency) are selected automatically, but generally are not visible in Market Watch. 327 | To be displayed such symbols have to be explicitly selected. 328 | 329 | :return: `SymbolInfoInteger(:symbol, SYMBOL_VISIBLE)` 330 | """ 331 | return self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_VISIBLE) 332 | 333 | @property 334 | def time(self) -> int: 335 | """Time of the last quote. 336 | 337 | :return: `SymbolInfoInteger(:symbol, SYMBOL_TIME)` 338 | """ 339 | return self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_TIME) 340 | 341 | @property 342 | def is_spread_float(self) -> bool: 343 | """Whether there is a floating spread. 344 | 345 | :return: `SymbolInfoInteger(:symbol, SYMBOL_SPREAD_FLOAT)` 346 | """ 347 | return self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_SPREAD_FLOAT) 348 | 349 | @property 350 | def spread(self) -> int: 351 | """Spread value in points. 352 | 353 | :return: `SymbolInfoInteger(:symbol, SYMBOL_SPREAD)` 354 | """ 355 | return self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_SPREAD) 356 | 357 | @property 358 | def trade_calc_mode(self) -> SymbolCalcMode: 359 | """Contract price calculation mode. 360 | 361 | :return: `SymbolInfoInteger(:symbol, SYMBOL_TRADE_CALC_MODE)` 362 | """ 363 | val = self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_TRADE_CALC_MODE) 364 | return SymbolCalcMode(val) 365 | 366 | @property 367 | def trade_mode(self) -> SymbolTradeMode: 368 | """Order execution type. 369 | 370 | :return: `SymbolInfoInteger(:symbol, SYMBOL_TRADE_MODE)` 371 | """ 372 | val = self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_TRADE_MODE) 373 | return SymbolTradeMode(val) 374 | 375 | @property 376 | def start_time(self) -> int: 377 | """Date of the symbol trade beginning (usually used for futures). 378 | 379 | :return: `SymbolInfoInteger(:symbol, SYMBOL_START_TIME)` 380 | """ 381 | return self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_START_TIME) 382 | 383 | @property 384 | def expiration_time(self) -> int: 385 | """Date of the symbol trade end (usually used for futures). 386 | 387 | :return: `SymbolInfoInteger(:symbol, SYMBOL_EXPIRATION_TIME)` 388 | """ 389 | return self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_EXPIRATION_TIME) 390 | 391 | @property 392 | def trade_exe_mode(self) -> SymbolTradeExecution: 393 | """Deal execution mode. 394 | 395 | :return: `SymbolInfoInteger(:symbol, SYMBOL_TRADE_EXEMODE)` 396 | """ 397 | val = self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_TRADE_EXEMODE) 398 | return SymbolTradeExecution(val) 399 | 400 | @property 401 | def swap_mode(self) -> SymbolSwapMode: 402 | """Swap calculation model. 403 | 404 | :return: `SymbolInfoInteger(:symbol, SYMBOL_SWAP_MODE)` 405 | """ 406 | val = self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_SWAP_MODE) 407 | return SymbolSwapMode(val) 408 | 409 | @property 410 | def swap_rollover3days(self) -> DayOfWeek: 411 | """Day of week to charge 3 days swap rollover. 412 | 413 | :return: `SymbolInfoInteger(:symbol, SYMBOL_SWAP_ROLLOVER3DAYS)` 414 | """ 415 | val = self._get_symbol_info_integer(SymbolInfoInteger.SYMBOL_SWAP_ROLLOVER3DAYS) 416 | return DayOfWeek(val) 417 | 418 | @property 419 | def bid(self) -> float: 420 | """Bid - best sell offer. 421 | 422 | :return: `SymbolInfoDouble(:symbol, SYMBOL_BID)` 423 | """ 424 | return self._get_symbol_info_double(SymbolInfoDouble.SYMBOL_BID) 425 | 426 | @property 427 | def ask(self) -> float: 428 | """Ask - best buy offer. 429 | 430 | :return: `SymbolInfoDouble(:symbol, SYMBOL_ASK)` 431 | """ 432 | return self._get_symbol_info_double(SymbolInfoDouble.SYMBOL_ASK) 433 | 434 | @property 435 | def swap_long(self) -> float: 436 | """Buy order swap value. 437 | 438 | :return: `SymbolInfoDouble(:symbol, SYMBOL_SWAP_LONG)` 439 | """ 440 | return self._get_symbol_info_double(SymbolInfoDouble.SYMBOL_SWAP_LONG) 441 | 442 | @property 443 | def swap_short(self) -> float: 444 | """Sell order swap value. 445 | 446 | :return: `SymbolInfoDouble(:symbol, SYMBOL_SWAP_SHORT)` 447 | """ 448 | return self._get_symbol_info_double(SymbolInfoDouble.SYMBOL_SWAP_SHORT) 449 | 450 | @property 451 | def margin_initial(self) -> float: 452 | """Initial margin means the amount in the margin currency required for opening an order with the volume of one 453 | lot. It is used for checking a client's assets when he or she enters the market. 454 | 455 | :return: `SymbolInfoDouble(:symbol, SYMBOL_MARGIN_INITIAL)` 456 | """ 457 | return self._get_symbol_info_double(SymbolInfoDouble.SYMBOL_MARGIN_INITIAL) 458 | 459 | @property 460 | def margin_maintenance(self) -> float: 461 | """The maintenance margin. If it is set, it sets the margin amount in the margin currency of the symbol, charged 462 | from one lot. It is used for checking a client's assets when his/her account state changes. If the maintenance 463 | margin is equal to 0, the initial margin is used. 464 | 465 | :return: `SymbolInfoDouble(:symbol, SYMBOL_MARGIN_MAINTENANCE)` 466 | """ 467 | return self._get_symbol_info_double(SymbolInfoDouble.SYMBOL_MARGIN_MAINTENANCE) 468 | 469 | def _get_symbol_info_integer(self, prop: SymbolInfoInteger) -> Union[bool, int]: 470 | return self._mt4._get_response(request={ 471 | "action": "GET_SYMBOL_INFO_INTEGER", 472 | "symbol": self.name, 473 | "property_name": prop.name 474 | }) 475 | 476 | def _get_symbol_info_double(self, prop: SymbolInfoDouble) -> float: 477 | return self._mt4._get_response(request={ 478 | "action": "GET_SYMBOL_INFO_DOUBLE", 479 | "symbol": self.name, 480 | "property_name": prop.name 481 | }) 482 | 483 | def __repr__(self): 484 | return (f'{self.__class__.__name__}(' 485 | f'name={self.name}, ' 486 | f'point_size={self.point}, ' 487 | f'digits={self.digits}, ' 488 | f'volume_min={self.volume_min}, ' 489 | f'volume_step={self.volume_step}, ' 490 | f'volume_max={self.volume_max}, ' 491 | f'trade_contract_size={self.trade_contract_size}, ' 492 | f'trade_tick_value={self.trade_tick_value}, ' 493 | f'trade_tick_size={self.trade_tick_size}, ' 494 | f'trade_stops_level={self.trade_stops_level}, ' 495 | f'trade_freeze_level={self.trade_freeze_level})') 496 | --------------------------------------------------------------------------------