├── 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 |
4 |
5 |
6 |
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 |
5 |
6 |
7 |
8 |
9 |
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 |
10 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------