PythonMetaTrader5 2.3.2__tar.gz → 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. pythonmetatrader5-3.0.0/LICENSE +16 -0
  2. pythonmetatrader5-3.0.0/PKG-INFO +231 -0
  3. pythonmetatrader5-3.0.0/README.md +196 -0
  4. pythonmetatrader5-3.0.0/pyproject.toml +85 -0
  5. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/__init__.py +73 -0
  6. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/adapters/__init__.py +1 -0
  7. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/adapters/mt5_mapping.py +92 -0
  8. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/adapters/mt5_session.py +48 -0
  9. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/adapters/mt5_terminal.py +224 -0
  10. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/adapters/system_clock.py +10 -0
  11. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/__init__.py +1 -0
  12. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/batch.py +60 -0
  13. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/entries.py +163 -0
  14. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/execution.py +108 -0
  15. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/magic_scope.py +65 -0
  16. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/ports.py +75 -0
  17. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/trader.py +275 -0
  18. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/connection.py +52 -0
  19. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/__init__.py +1 -0
  20. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/amounts.py +33 -0
  21. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/errors.py +53 -0
  22. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/inputs.py +106 -0
  23. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/market.py +91 -0
  24. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/orders.py +64 -0
  25. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/requests.py +32 -0
  26. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/retcodes.py +81 -0
  27. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/stops.py +85 -0
  28. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/symbol.py +158 -0
  29. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/legacy/__init__.py +1 -0
  30. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/legacy/arguments.py +52 -0
  31. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/legacy/broker.py +284 -0
  32. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/legacy/mt5_namespace.py +56 -0
  33. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/legacy/views.py +123 -0
  34. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/py.typed +0 -0
  35. pythonmetatrader5-3.0.0/src/PythonMetaTrader5/result.py +41 -0
  36. pythonmetatrader5-3.0.0/src/PythonMetaTrader5.egg-info/PKG-INFO +231 -0
  37. pythonmetatrader5-3.0.0/src/PythonMetaTrader5.egg-info/SOURCES.txt +39 -0
  38. pythonmetatrader5-3.0.0/src/PythonMetaTrader5.egg-info/requires.txt +11 -0
  39. pythonmetatrader5-2.3.2/PKG-INFO +0 -267
  40. pythonmetatrader5-2.3.2/README.md +0 -250
  41. pythonmetatrader5-2.3.2/pyproject.toml +0 -39
  42. pythonmetatrader5-2.3.2/src/PythonMetaTrader5/__init__.py +0 -535
  43. pythonmetatrader5-2.3.2/src/PythonMetaTrader5.egg-info/PKG-INFO +0 -267
  44. pythonmetatrader5-2.3.2/src/PythonMetaTrader5.egg-info/SOURCES.txt +0 -8
  45. pythonmetatrader5-2.3.2/src/PythonMetaTrader5.egg-info/requires.txt +0 -3
  46. {pythonmetatrader5-2.3.2 → pythonmetatrader5-3.0.0}/setup.cfg +0 -0
  47. {pythonmetatrader5-2.3.2 → pythonmetatrader5-3.0.0}/src/PythonMetaTrader5.egg-info/dependency_links.txt +0 -0
  48. {pythonmetatrader5-2.3.2 → pythonmetatrader5-3.0.0}/src/PythonMetaTrader5.egg-info/top_level.txt +0 -0
@@ -0,0 +1,16 @@
1
+ PythonMetaTrader5
2
+ Copyright (c) Wiktor Jelen
3
+
4
+ This work is licensed under the Creative Commons Attribution-NonCommercial 4.0
5
+ International License (CC BY-NC 4.0).
6
+
7
+ You are free to share and adapt this work for non-commercial purposes, provided
8
+ you give appropriate credit, link to the license, and indicate if changes were
9
+ made.
10
+
11
+ Full legal code: https://creativecommons.org/licenses/by-nc/4.0/legalcode
12
+ Summary: https://creativecommons.org/licenses/by-nc/4.0/
13
+
14
+ THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. TRADING FINANCIAL
15
+ INSTRUMENTS CARRIES RISK; THE AUTHORS ARE NOT LIABLE FOR ANY LOSSES INCURRED
16
+ THROUGH THE USE OF THIS SOFTWARE.
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: PythonMetaTrader5
3
+ Version: 3.0.0
4
+ Summary: Safe, typed trading layer over the official MetaTrader5 Python API
5
+ Author-email: Wiktor Jelen <wiktorjn@gmail.com>
6
+ License-Expression: CC-BY-NC-4.0
7
+ Project-URL: Homepage, https://github.com/Akinzou/MetaTrader5-Python
8
+ Project-URL: Bug Tracker, https://github.com/Akinzou/MetaTrader5-Python/issues
9
+ Project-URL: Changelog, https://github.com/Akinzou/MetaTrader5-Python/blob/master/CHANGELOG.md
10
+ Keywords: metatrader5,mt5,trading,forex,algotrading
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Office/Business :: Financial :: Investment
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: MetaTrader5; sys_platform == "win32"
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8; extra == "dev"
29
+ Requires-Dist: pytest-cov>=5; extra == "dev"
30
+ Requires-Dist: ruff>=0.6; extra == "dev"
31
+ Requires-Dist: mypy>=1.10; extra == "dev"
32
+ Requires-Dist: build>=1.2; extra == "dev"
33
+ Requires-Dist: twine>=5; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # PythonMetaTrader5
37
+
38
+ [![CI](https://github.com/Akinzou/MetaTrader5-Python/actions/workflows/ci.yml/badge.svg)](https://github.com/Akinzou/MetaTrader5-Python/actions/workflows/ci.yml)
39
+ [![PyPI](https://img.shields.io/pypi/v/pythonmetatrader5)](https://pypi.org/project/PythonMetaTrader5/)
40
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue?logo=python&logoColor=white)
41
+ ![License](https://img.shields.io/badge/license-CC_BY--NC_4.0-lightgrey.svg)
42
+ [![PyPI Downloads](https://static.pepy.tech/badge/pythonmetatrader5)](https://pepy.tech/projects/pythonmetatrader5)
43
+ [![Discord](https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white)](https://discord.gg/BARYa55KS8)
44
+
45
+ A safe, typed trading layer over the official
46
+ [`MetaTrader5`](https://pypi.org/project/MetaTrader5/) Python package.
47
+
48
+ Built for bots (webhook executors, signal followers) where a trading call must
49
+ **never hang, never retry blindly, and never fail silently**:
50
+
51
+ - **Every trading call returns.** Requotes are retried a bounded number of times
52
+ with a fresh quote; everything else comes back immediately as a typed error.
53
+ (The deprecated `Broker.login(ToEffect=True)` is the one exception: as in
54
+ 2.x, it keeps trying until the login works.)
55
+ - **No silent defaults.** Values that MT5 would quietly read as something else
56
+ are refused before sending: `sl=0` ("no stop"), `ticket=0` (possibly "no
57
+ filter"), `volume=True` (one lot), NaN and infinities.
58
+ - **No double fills.** Timeouts and lost connections are never auto-retried:
59
+ the order may already be live.
60
+ - **Typed results.** `Ok(receipt)` or `Err(TradeError)` with an `ErrorKind`
61
+ (`MARKET_CLOSED`, `NO_MONEY`, `INVALID_STOPS`, ...) instead of magic numbers.
62
+ - **Correct prices and lots.** Prices snap to the symbol's tick grid, volumes
63
+ are checked against min/max/step, the filling mode is chosen from the symbol.
64
+ - **Magic numbers.** Tag your bot's trades and close/cancel only those.
65
+ - **SL/TP as prices or distances in points.**
66
+ - **2.x compatible.** The old `Broker` class still works (deprecated).
67
+
68
+ > One connection per process: the official package keeps a single global
69
+ > terminal session, so a second `connect()` (or its failed login) affects the
70
+ > first. Run one process per account.
71
+ >
72
+ > Thread-safe within the library: every terminal call holds one process-wide
73
+ > lock, so a threaded webhook server can share a `Trader`. The MT5 functions
74
+ > re-exported through `PythonMetaTrader5` hold it for each single call (a pair
75
+ > such as `order_send` + `last_error` is not atomic). Calls you make to the
76
+ > `MetaTrader5` module directly bypass it.
77
+ >
78
+ > Requires Windows with a MetaTrader 5 terminal installed (the official
79
+ > `MetaTrader5` package is Windows-only). Enable **Algo Trading** in the terminal.
80
+
81
+ ## Installation
82
+
83
+ ```bash
84
+ pip install PythonMetaTrader5
85
+ ```
86
+
87
+ ## Quick start
88
+
89
+ ```python
90
+ from PythonMetaTrader5 import Err, Ok, Side, Stop, connect
91
+
92
+ match connect(12345678, "password", "MetaQuotes-Demo", magic=1001):
93
+ case Err(error):
94
+ raise SystemExit(f"cannot connect: {error}")
95
+ case Ok(trader):
96
+ pass
97
+
98
+ with trader: # shuts the terminal connection down on exit
99
+ result = trader.open_position(
100
+ "EURUSD", Side.BUY, 0.10,
101
+ sl=Stop.points(1000), # 1000 points below the entry price
102
+ tp=1.12500, # a plain float is an absolute price
103
+ comment="webhook #42",
104
+ )
105
+ match result:
106
+ case Ok(receipt):
107
+ print("filled at", receipt.price, "deal", receipt.deal)
108
+ case Err(error):
109
+ print("not opened:", error.kind, error.message)
110
+
111
+ trader.close_positions("EURUSD") # only this bot's (magic 1001) positions
112
+ ```
113
+
114
+ ## API
115
+
116
+ `connect(login, password, server, *, path=None, timeout_ms=60000, magic=0,
117
+ deviation=10, retry=None) -> Result[Trader, TradeError]`
118
+
119
+ `login` must be a positive integer or a string of ASCII digits; `True`,
120
+ `12345678.9` and similar values are rejected rather than coerced.
121
+
122
+ | `Trader` method | Returns |
123
+ |---|---|
124
+ | `open_position(symbol, side, volume, *, sl=None, tp=None, price=None, comment=None)` | `Result[TradeReceipt, TradeError]` |
125
+ | `place_limit_order(symbol, side, volume, price, *, sl=None, tp=None, comment=None)` | `Result[TradeReceipt, TradeError]` |
126
+ | `close_positions(symbol=None, *, ticket=None, magic=<own>, comment=None)` | `Result[list[TradeReceipt], BatchFailure]` |
127
+ | `cancel_pending_orders(symbol=None, *, magic=<own>, kind=None)` | `Result[list[TradeReceipt], BatchFailure]` |
128
+ | `positions(symbol=None, *, ticket=None, magic=None)` | `Result[list[Position], TradeError]` |
129
+ | `pending_orders(symbol=None, *, magic=None, kind=None)` | `Result[list[PendingOrder], TradeError]` |
130
+ | `shutdown()` | `None` |
131
+
132
+ - `side`: `Side.BUY` / `Side.SELL`, or the strings `"buy"`, `"sell"`, `"long"`, `"short"`.
133
+ - `sl` / `tp`: a float is an **absolute price**; `Stop.points(n)` is a distance
134
+ of `n` points from the entry price (`Stop.at_price(p)` is the explicit form).
135
+ For limit orders the distance is measured from the limit price, and for a
136
+ market order with an explicit `price` from that price. A float stop that is
137
+ zero, negative or not finite is rejected (`INVALID_STOPS`); pass `None` for
138
+ no stop. `Stop.points()` / `Stop.at_price()` raise `ValueError` unless the
139
+ value is a positive finite number (bool and str included; a zero stop would
140
+ mean "no stop" to MT5); the sign of a points distance is ignored. A `kind`
141
+ that is not a `StopKind` raises `TypeError`. A stop that resolves outside
142
+ the tick grid's range, or to zero or below, is `INVALID_STOPS` when the
143
+ order is built. For a value that may be bad at runtime, such as
144
+ an ATR that is NaN during warm-up, use
145
+ `Stop.parse(value, StopKind.POINTS)`, which returns `Ok(stop)` /
146
+ `Err(INVALID_STOPS)`.
147
+ - Amounts may be `int`, `float`, `Decimal` or numpy scalars; `bool` and strings
148
+ are refused (the deprecated `Broker` still converts numeric strings).
149
+ - `price` (market orders): send at this price once instead of the live quote.
150
+ - `comment`: at most 29 UTF-16 units: 29 characters, but characters outside
151
+ the BMP, such as most emoji, count as 2 (combined emoji more). The `MetaTrader5` package refuses 30 or more (measured on 5.0.6231; its
152
+ docs say 31). Longer comments, and comments that are not `str`, are rejected
153
+ before sending, not truncated.
154
+ - **`close_positions()` and `cancel_pending_orders()` act only on the trader's
155
+ own `magic`** unless you pass another one (ADR 0011); `magic=None` is still
156
+ "own". To act on the whole account, manual trades included, pass
157
+ `magic=ANY_MAGIC`. A `ticket` is closed whatever its magic. `positions()`
158
+ and `pending_orders()` still list every magic by default.
159
+ - Manual trades have magic 0, the `connect()` default, so set your own `magic`
160
+ for this to protect them. Until then a close/cancel that affects magic-0
161
+ items logs a warning; `magic=0` passed explicitly acknowledges it. On a
162
+ netting account a position keeps the magic of the deal that opened it, so
163
+ merged volume cannot be told apart by magic; a defaulted close/cancel that
164
+ matches nothing logs how many items with other magics it left open.
165
+ - A magic must be an integer from 0 to 2\*\*64-1 (MT5's ulong); `connect()`
166
+ returns `Err(INVALID_ARGUMENT)` otherwise, e.g. for `None`. An empty string
167
+ `""` as symbol is rejected too.
168
+ - `BatchFailure.errors` names every position/order that failed
169
+ (`TradeError.ticket`). `BatchFailure.succeeded` holds the receipts of the
170
+ ones that worked; their `order` / `deal` are the IDs of the closing deal, not
171
+ the position ticket.
172
+ - `ticket` must be a positive integer; `0`, `True` or `"7"` are rejected.
173
+
174
+ ### Errors
175
+
176
+ `TradeError(kind, message, retcode=None, receipt=None, ticket=None)`. The
177
+ `receipt` carries the broker's raw answer when there was one. Retry policy:
178
+
179
+ | Server answer | What happens |
180
+ |---|---|
181
+ | market deal (entry or close): done | `Ok(receipt)` |
182
+ | pending order: placed or done | `Ok(receipt)` |
183
+ | market deal: partial fill | `PARTIAL_FILL` with the receipt; part of the deal exists, never re-sent |
184
+ | market deal: placed (queued, not executed) | `OUTCOME_UNKNOWN` with the receipt, never re-sent |
185
+ | requote, price changed, no quotes, too many requests | retried with a fresh quote, up to `RetryPolicy.attempts` (default 3, 0.5 s apart), then `RETRIES_EXHAUSTED` |
186
+ | timeout, no connection, or `order_send` lost in IPC | `OUTCOME_UNKNOWN`, never retried: check `positions()` first |
187
+ | anything else | the matching `ErrorKind`, returned at once |
188
+ | refused before sending: the terminal's symbol data cannot price an order (point 0, NaN tick size, volume step 0, ...) | `BROKEN_SYMBOL_DATA`, the same on every resend: escalate, don't retry. Closing still works on such symbols |
189
+
190
+ ```python
191
+ from PythonMetaTrader5 import RetryPolicy, connect
192
+
193
+ connect(..., retry=RetryPolicy(attempts=5, delay_seconds=0.2))
194
+ ```
195
+
196
+ ### Logging
197
+
198
+ The library logs through the standard `logging` module under the
199
+ `PythonMetaTrader5` logger and never configures handlers itself. Without any
200
+ logging configuration, warnings and errors still reach stderr.
201
+
202
+ ## Migrating from 2.x
203
+
204
+ `from PythonMetaTrader5 import Broker` keeps working and emits a
205
+ `DeprecationWarning`. `from PythonMetaTrader5 import *` still brings in the
206
+ `MetaTrader5` functions and constants, as in 2.x. Several 2.x behaviours were unsafe and changed: see
207
+ [MIGRATION.md](MIGRATION.md).
208
+
209
+ ## Development
210
+
211
+ ```bash
212
+ python -m venv .venv && .venv\Scripts\activate
213
+ pip install -e ".[dev]"
214
+ ruff check src tests scripts && ruff format --check src tests scripts
215
+ mypy
216
+ pytest --cov
217
+ ```
218
+
219
+ `tests/contract` compares the unit-test fakes with the installed `MetaTrader5`
220
+ package and is skipped where it is not installed (anywhere but Windows).
221
+ Integration tests trade on a **demo** account and are skipped unless
222
+ `MT5_LOGIN`, `MT5_PASSWORD` and `MT5_SERVER` are set (they refuse to run on a
223
+ live account): `pytest -m integration`.
224
+
225
+ Architecture and decisions: [docs/architecture.md](docs/architecture.md),
226
+ [docs/adr](docs/adr).
227
+
228
+ ## License
229
+
230
+ [CC BY-NC 4.0](LICENSE). Trading carries risk; this software comes without any
231
+ warranty.
@@ -0,0 +1,196 @@
1
+ # PythonMetaTrader5
2
+
3
+ [![CI](https://github.com/Akinzou/MetaTrader5-Python/actions/workflows/ci.yml/badge.svg)](https://github.com/Akinzou/MetaTrader5-Python/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/pythonmetatrader5)](https://pypi.org/project/PythonMetaTrader5/)
5
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue?logo=python&logoColor=white)
6
+ ![License](https://img.shields.io/badge/license-CC_BY--NC_4.0-lightgrey.svg)
7
+ [![PyPI Downloads](https://static.pepy.tech/badge/pythonmetatrader5)](https://pepy.tech/projects/pythonmetatrader5)
8
+ [![Discord](https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white)](https://discord.gg/BARYa55KS8)
9
+
10
+ A safe, typed trading layer over the official
11
+ [`MetaTrader5`](https://pypi.org/project/MetaTrader5/) Python package.
12
+
13
+ Built for bots (webhook executors, signal followers) where a trading call must
14
+ **never hang, never retry blindly, and never fail silently**:
15
+
16
+ - **Every trading call returns.** Requotes are retried a bounded number of times
17
+ with a fresh quote; everything else comes back immediately as a typed error.
18
+ (The deprecated `Broker.login(ToEffect=True)` is the one exception: as in
19
+ 2.x, it keeps trying until the login works.)
20
+ - **No silent defaults.** Values that MT5 would quietly read as something else
21
+ are refused before sending: `sl=0` ("no stop"), `ticket=0` (possibly "no
22
+ filter"), `volume=True` (one lot), NaN and infinities.
23
+ - **No double fills.** Timeouts and lost connections are never auto-retried:
24
+ the order may already be live.
25
+ - **Typed results.** `Ok(receipt)` or `Err(TradeError)` with an `ErrorKind`
26
+ (`MARKET_CLOSED`, `NO_MONEY`, `INVALID_STOPS`, ...) instead of magic numbers.
27
+ - **Correct prices and lots.** Prices snap to the symbol's tick grid, volumes
28
+ are checked against min/max/step, the filling mode is chosen from the symbol.
29
+ - **Magic numbers.** Tag your bot's trades and close/cancel only those.
30
+ - **SL/TP as prices or distances in points.**
31
+ - **2.x compatible.** The old `Broker` class still works (deprecated).
32
+
33
+ > One connection per process: the official package keeps a single global
34
+ > terminal session, so a second `connect()` (or its failed login) affects the
35
+ > first. Run one process per account.
36
+ >
37
+ > Thread-safe within the library: every terminal call holds one process-wide
38
+ > lock, so a threaded webhook server can share a `Trader`. The MT5 functions
39
+ > re-exported through `PythonMetaTrader5` hold it for each single call (a pair
40
+ > such as `order_send` + `last_error` is not atomic). Calls you make to the
41
+ > `MetaTrader5` module directly bypass it.
42
+ >
43
+ > Requires Windows with a MetaTrader 5 terminal installed (the official
44
+ > `MetaTrader5` package is Windows-only). Enable **Algo Trading** in the terminal.
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install PythonMetaTrader5
50
+ ```
51
+
52
+ ## Quick start
53
+
54
+ ```python
55
+ from PythonMetaTrader5 import Err, Ok, Side, Stop, connect
56
+
57
+ match connect(12345678, "password", "MetaQuotes-Demo", magic=1001):
58
+ case Err(error):
59
+ raise SystemExit(f"cannot connect: {error}")
60
+ case Ok(trader):
61
+ pass
62
+
63
+ with trader: # shuts the terminal connection down on exit
64
+ result = trader.open_position(
65
+ "EURUSD", Side.BUY, 0.10,
66
+ sl=Stop.points(1000), # 1000 points below the entry price
67
+ tp=1.12500, # a plain float is an absolute price
68
+ comment="webhook #42",
69
+ )
70
+ match result:
71
+ case Ok(receipt):
72
+ print("filled at", receipt.price, "deal", receipt.deal)
73
+ case Err(error):
74
+ print("not opened:", error.kind, error.message)
75
+
76
+ trader.close_positions("EURUSD") # only this bot's (magic 1001) positions
77
+ ```
78
+
79
+ ## API
80
+
81
+ `connect(login, password, server, *, path=None, timeout_ms=60000, magic=0,
82
+ deviation=10, retry=None) -> Result[Trader, TradeError]`
83
+
84
+ `login` must be a positive integer or a string of ASCII digits; `True`,
85
+ `12345678.9` and similar values are rejected rather than coerced.
86
+
87
+ | `Trader` method | Returns |
88
+ |---|---|
89
+ | `open_position(symbol, side, volume, *, sl=None, tp=None, price=None, comment=None)` | `Result[TradeReceipt, TradeError]` |
90
+ | `place_limit_order(symbol, side, volume, price, *, sl=None, tp=None, comment=None)` | `Result[TradeReceipt, TradeError]` |
91
+ | `close_positions(symbol=None, *, ticket=None, magic=<own>, comment=None)` | `Result[list[TradeReceipt], BatchFailure]` |
92
+ | `cancel_pending_orders(symbol=None, *, magic=<own>, kind=None)` | `Result[list[TradeReceipt], BatchFailure]` |
93
+ | `positions(symbol=None, *, ticket=None, magic=None)` | `Result[list[Position], TradeError]` |
94
+ | `pending_orders(symbol=None, *, magic=None, kind=None)` | `Result[list[PendingOrder], TradeError]` |
95
+ | `shutdown()` | `None` |
96
+
97
+ - `side`: `Side.BUY` / `Side.SELL`, or the strings `"buy"`, `"sell"`, `"long"`, `"short"`.
98
+ - `sl` / `tp`: a float is an **absolute price**; `Stop.points(n)` is a distance
99
+ of `n` points from the entry price (`Stop.at_price(p)` is the explicit form).
100
+ For limit orders the distance is measured from the limit price, and for a
101
+ market order with an explicit `price` from that price. A float stop that is
102
+ zero, negative or not finite is rejected (`INVALID_STOPS`); pass `None` for
103
+ no stop. `Stop.points()` / `Stop.at_price()` raise `ValueError` unless the
104
+ value is a positive finite number (bool and str included; a zero stop would
105
+ mean "no stop" to MT5); the sign of a points distance is ignored. A `kind`
106
+ that is not a `StopKind` raises `TypeError`. A stop that resolves outside
107
+ the tick grid's range, or to zero or below, is `INVALID_STOPS` when the
108
+ order is built. For a value that may be bad at runtime, such as
109
+ an ATR that is NaN during warm-up, use
110
+ `Stop.parse(value, StopKind.POINTS)`, which returns `Ok(stop)` /
111
+ `Err(INVALID_STOPS)`.
112
+ - Amounts may be `int`, `float`, `Decimal` or numpy scalars; `bool` and strings
113
+ are refused (the deprecated `Broker` still converts numeric strings).
114
+ - `price` (market orders): send at this price once instead of the live quote.
115
+ - `comment`: at most 29 UTF-16 units: 29 characters, but characters outside
116
+ the BMP, such as most emoji, count as 2 (combined emoji more). The `MetaTrader5` package refuses 30 or more (measured on 5.0.6231; its
117
+ docs say 31). Longer comments, and comments that are not `str`, are rejected
118
+ before sending, not truncated.
119
+ - **`close_positions()` and `cancel_pending_orders()` act only on the trader's
120
+ own `magic`** unless you pass another one (ADR 0011); `magic=None` is still
121
+ "own". To act on the whole account, manual trades included, pass
122
+ `magic=ANY_MAGIC`. A `ticket` is closed whatever its magic. `positions()`
123
+ and `pending_orders()` still list every magic by default.
124
+ - Manual trades have magic 0, the `connect()` default, so set your own `magic`
125
+ for this to protect them. Until then a close/cancel that affects magic-0
126
+ items logs a warning; `magic=0` passed explicitly acknowledges it. On a
127
+ netting account a position keeps the magic of the deal that opened it, so
128
+ merged volume cannot be told apart by magic; a defaulted close/cancel that
129
+ matches nothing logs how many items with other magics it left open.
130
+ - A magic must be an integer from 0 to 2\*\*64-1 (MT5's ulong); `connect()`
131
+ returns `Err(INVALID_ARGUMENT)` otherwise, e.g. for `None`. An empty string
132
+ `""` as symbol is rejected too.
133
+ - `BatchFailure.errors` names every position/order that failed
134
+ (`TradeError.ticket`). `BatchFailure.succeeded` holds the receipts of the
135
+ ones that worked; their `order` / `deal` are the IDs of the closing deal, not
136
+ the position ticket.
137
+ - `ticket` must be a positive integer; `0`, `True` or `"7"` are rejected.
138
+
139
+ ### Errors
140
+
141
+ `TradeError(kind, message, retcode=None, receipt=None, ticket=None)`. The
142
+ `receipt` carries the broker's raw answer when there was one. Retry policy:
143
+
144
+ | Server answer | What happens |
145
+ |---|---|
146
+ | market deal (entry or close): done | `Ok(receipt)` |
147
+ | pending order: placed or done | `Ok(receipt)` |
148
+ | market deal: partial fill | `PARTIAL_FILL` with the receipt; part of the deal exists, never re-sent |
149
+ | market deal: placed (queued, not executed) | `OUTCOME_UNKNOWN` with the receipt, never re-sent |
150
+ | requote, price changed, no quotes, too many requests | retried with a fresh quote, up to `RetryPolicy.attempts` (default 3, 0.5 s apart), then `RETRIES_EXHAUSTED` |
151
+ | timeout, no connection, or `order_send` lost in IPC | `OUTCOME_UNKNOWN`, never retried: check `positions()` first |
152
+ | anything else | the matching `ErrorKind`, returned at once |
153
+ | refused before sending: the terminal's symbol data cannot price an order (point 0, NaN tick size, volume step 0, ...) | `BROKEN_SYMBOL_DATA`, the same on every resend: escalate, don't retry. Closing still works on such symbols |
154
+
155
+ ```python
156
+ from PythonMetaTrader5 import RetryPolicy, connect
157
+
158
+ connect(..., retry=RetryPolicy(attempts=5, delay_seconds=0.2))
159
+ ```
160
+
161
+ ### Logging
162
+
163
+ The library logs through the standard `logging` module under the
164
+ `PythonMetaTrader5` logger and never configures handlers itself. Without any
165
+ logging configuration, warnings and errors still reach stderr.
166
+
167
+ ## Migrating from 2.x
168
+
169
+ `from PythonMetaTrader5 import Broker` keeps working and emits a
170
+ `DeprecationWarning`. `from PythonMetaTrader5 import *` still brings in the
171
+ `MetaTrader5` functions and constants, as in 2.x. Several 2.x behaviours were unsafe and changed: see
172
+ [MIGRATION.md](MIGRATION.md).
173
+
174
+ ## Development
175
+
176
+ ```bash
177
+ python -m venv .venv && .venv\Scripts\activate
178
+ pip install -e ".[dev]"
179
+ ruff check src tests scripts && ruff format --check src tests scripts
180
+ mypy
181
+ pytest --cov
182
+ ```
183
+
184
+ `tests/contract` compares the unit-test fakes with the installed `MetaTrader5`
185
+ package and is skipped where it is not installed (anywhere but Windows).
186
+ Integration tests trade on a **demo** account and are skipped unless
187
+ `MT5_LOGIN`, `MT5_PASSWORD` and `MT5_SERVER` are set (they refuse to run on a
188
+ live account): `pytest -m integration`.
189
+
190
+ Architecture and decisions: [docs/architecture.md](docs/architecture.md),
191
+ [docs/adr](docs/adr).
192
+
193
+ ## License
194
+
195
+ [CC BY-NC 4.0](LICENSE). Trading carries risk; this software comes without any
196
+ warranty.
@@ -0,0 +1,85 @@
1
+ [build-system]
2
+ # setuptools>=77 is the first release that accepts a PEP 639 SPDX `license` string;
3
+ # older versions would silently publish the wrong license metadata again.
4
+ requires = ["setuptools>=77", "wheel"]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "PythonMetaTrader5"
9
+ version = "3.0.0"
10
+ description = "Safe, typed trading layer over the official MetaTrader5 Python API"
11
+ readme = "README.md"
12
+ requires-python = ">=3.10"
13
+ license = "CC-BY-NC-4.0"
14
+ license-files = ["LICENSE"]
15
+ authors = [{ name = "Wiktor Jelen", email = "wiktorjn@gmail.com" }]
16
+ keywords = ["metatrader5", "mt5", "trading", "forex", "algotrading"]
17
+ classifiers = [
18
+ "Development Status :: 5 - Production/Stable",
19
+ "Intended Audience :: Developers",
20
+ "Intended Audience :: Financial and Insurance Industry",
21
+ "Operating System :: Microsoft :: Windows",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Topic :: Office/Business :: Financial :: Investment",
29
+ "Typing :: Typed",
30
+ ]
31
+ # The official MetaTrader5 package ships Windows wheels only. The marker keeps the
32
+ # package installable on Linux/macOS (CI, type checking, unit tests against fakes)
33
+ # instead of failing the whole install.
34
+ dependencies = ["MetaTrader5; sys_platform == 'win32'"]
35
+
36
+ [project.optional-dependencies]
37
+ dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6", "mypy>=1.10", "build>=1.2", "twine>=5"]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/Akinzou/MetaTrader5-Python"
41
+ "Bug Tracker" = "https://github.com/Akinzou/MetaTrader5-Python/issues"
42
+ Changelog = "https://github.com/Akinzou/MetaTrader5-Python/blob/master/CHANGELOG.md"
43
+
44
+ [tool.setuptools.packages.find]
45
+ where = ["src"]
46
+
47
+ [tool.setuptools.package-data]
48
+ PythonMetaTrader5 = ["py.typed"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+ addopts = "-ra --strict-markers"
53
+ markers = ["integration: needs a running MetaTrader 5 terminal and MT5_* credentials"]
54
+
55
+ [tool.coverage.run]
56
+ source = ["PythonMetaTrader5"]
57
+ branch = true
58
+
59
+ [tool.coverage.report]
60
+ fail_under = 95
61
+ show_missing = true
62
+ exclude_also = ["if TYPE_CHECKING:", "raise AssertionError"]
63
+
64
+ [tool.ruff]
65
+ line-length = 100
66
+ target-version = "py310"
67
+
68
+ [tool.ruff.lint]
69
+ select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "N", "S", "PL", "C4", "PT", "RET"]
70
+ # N999: the import name `PythonMetaTrader5` predates this rewrite; renaming it to
71
+ # snake_case would break every existing `from PythonMetaTrader5 import Broker`.
72
+ ignore = ["PLR0913", "N999"]
73
+
74
+ [tool.ruff.lint.per-file-ignores]
75
+ # The 2.x public method names (Buy, GetPositions, ToEffect...) are the compatibility contract.
76
+ # PLR0917: BuyLimit/SellLimit accepted price, comment and ticket positionally in 2.x.
77
+ "src/PythonMetaTrader5/legacy/*.py" = ["N802", "N803", "N815", "PLR0917"]
78
+ # S106: tests use throwaway fake credentials.
79
+ "tests/**/*.py" = ["S101", "S106", "PLR2004"]
80
+
81
+ [tool.mypy]
82
+ strict = true
83
+ python_version = "3.10"
84
+ mypy_path = "src"
85
+ packages = ["PythonMetaTrader5"]
@@ -0,0 +1,73 @@
1
+ """PythonMetaTrader5: a safe, typed trading layer over the MetaTrader5 package.
2
+
3
+ from PythonMetaTrader5 import connect, Stop, Ok, Err
4
+
5
+ match connect(12345678, "password", "MetaQuotes-Demo", magic=1001):
6
+ case Ok(trader):
7
+ trader.open_position("EURUSD", "buy", 0.1, sl=Stop.points(1000))
8
+ case Err(error):
9
+ print(error)
10
+ """
11
+
12
+ from typing import Any
13
+
14
+ # `X as X`: explicit re-exports, because __all__ is computed (see __getattr__).
15
+ from PythonMetaTrader5.application.batch import BatchFailure as BatchFailure
16
+ from PythonMetaTrader5.application.execution import RetryPolicy as RetryPolicy
17
+ from PythonMetaTrader5.application.trader import Trader as Trader
18
+ from PythonMetaTrader5.connection import connect as connect
19
+ from PythonMetaTrader5.domain.errors import ErrorKind as ErrorKind
20
+ from PythonMetaTrader5.domain.errors import TradeError as TradeError
21
+ from PythonMetaTrader5.domain.inputs import ANY_MAGIC as ANY_MAGIC
22
+ from PythonMetaTrader5.domain.inputs import AnyMagic as AnyMagic
23
+ from PythonMetaTrader5.domain.market import PendingKind as PendingKind
24
+ from PythonMetaTrader5.domain.market import PendingOrder as PendingOrder
25
+ from PythonMetaTrader5.domain.market import Position as Position
26
+ from PythonMetaTrader5.domain.market import Tick as Tick
27
+ from PythonMetaTrader5.domain.market import TradeReceipt as TradeReceipt
28
+ from PythonMetaTrader5.domain.orders import Side as Side
29
+ from PythonMetaTrader5.domain.stops import Stop as Stop
30
+ from PythonMetaTrader5.domain.stops import StopKind as StopKind
31
+ from PythonMetaTrader5.legacy import mt5_namespace as _mt5_namespace
32
+ from PythonMetaTrader5.legacy.broker import Broker as Broker
33
+ from PythonMetaTrader5.result import Err as Err
34
+ from PythonMetaTrader5.result import Ok as Ok
35
+ from PythonMetaTrader5.result import Result as Result
36
+
37
+ _OWN_NAMES = (
38
+ "ANY_MAGIC",
39
+ "AnyMagic",
40
+ "BatchFailure",
41
+ "Broker",
42
+ "Err",
43
+ "ErrorKind",
44
+ "Ok",
45
+ "PendingKind",
46
+ "PendingOrder",
47
+ "Position",
48
+ "Result",
49
+ "RetryPolicy",
50
+ "Side",
51
+ "Stop",
52
+ "StopKind",
53
+ "Tick",
54
+ "TradeError",
55
+ "TradeReceipt",
56
+ "Trader",
57
+ "connect",
58
+ )
59
+
60
+
61
+ def __getattr__(name: str) -> Any:
62
+ # 2.x re-exported the whole MetaTrader5 namespace and its README relied on it
63
+ # (`symbol_info("EURUSD")` after a star import), so those names resolve here,
64
+ # lazily (ADR 0010). A star import asks for __all__ through this hook too.
65
+ # Names defined in this module always win, e.g. our Tick over MT5's struct.
66
+ if name == "__all__":
67
+ mt5_names = _mt5_namespace.public_names()
68
+ return [*_OWN_NAMES, *(n for n in mt5_names if n not in globals())]
69
+ return _mt5_namespace.resolve(name)
70
+
71
+
72
+ def __dir__() -> list[str]:
73
+ return sorted({*globals(), *__getattr__("__all__")})
@@ -0,0 +1 @@
1
+ """Implementations of the application ports for the real world."""