PythonMetaTrader5 2.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.
- pythonmetatrader5-3.0.0/LICENSE +16 -0
- pythonmetatrader5-3.0.0/PKG-INFO +231 -0
- pythonmetatrader5-3.0.0/README.md +196 -0
- pythonmetatrader5-3.0.0/pyproject.toml +85 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/__init__.py +73 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/adapters/__init__.py +1 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/adapters/mt5_mapping.py +92 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/adapters/mt5_session.py +48 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/adapters/mt5_terminal.py +224 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/adapters/system_clock.py +10 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/__init__.py +1 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/batch.py +60 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/entries.py +163 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/execution.py +108 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/magic_scope.py +65 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/ports.py +75 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/application/trader.py +275 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/connection.py +52 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/__init__.py +1 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/amounts.py +33 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/errors.py +53 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/inputs.py +106 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/market.py +91 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/orders.py +64 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/requests.py +32 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/retcodes.py +81 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/stops.py +85 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/domain/symbol.py +158 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/legacy/__init__.py +1 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/legacy/arguments.py +52 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/legacy/broker.py +284 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/legacy/mt5_namespace.py +56 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/legacy/views.py +123 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/py.typed +0 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5/result.py +41 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5.egg-info/PKG-INFO +231 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5.egg-info/SOURCES.txt +39 -0
- pythonmetatrader5-3.0.0/src/PythonMetaTrader5.egg-info/requires.txt +11 -0
- pythonmetatrader5-2.2/PKG-INFO +0 -29
- pythonmetatrader5-2.2/README.md +0 -12
- pythonmetatrader5-2.2/pyproject.toml +0 -35
- pythonmetatrader5-2.2/src/PythonMetaTrader5/__init__.py +0 -535
- pythonmetatrader5-2.2/src/PythonMetaTrader5.egg-info/PKG-INFO +0 -29
- pythonmetatrader5-2.2/src/PythonMetaTrader5.egg-info/SOURCES.txt +0 -8
- pythonmetatrader5-2.2/src/PythonMetaTrader5.egg-info/requires.txt +0 -3
- {pythonmetatrader5-2.2 → pythonmetatrader5-3.0.0}/setup.cfg +0 -0
- {pythonmetatrader5-2.2 → pythonmetatrader5-3.0.0}/src/PythonMetaTrader5.egg-info/dependency_links.txt +0 -0
- {pythonmetatrader5-2.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
|
+
[](https://github.com/Akinzou/MetaTrader5-Python/actions/workflows/ci.yml)
|
|
39
|
+
[](https://pypi.org/project/PythonMetaTrader5/)
|
|
40
|
+

|
|
41
|
+

|
|
42
|
+
[](https://pepy.tech/projects/pythonmetatrader5)
|
|
43
|
+
[](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
|
+
[](https://github.com/Akinzou/MetaTrader5-Python/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/PythonMetaTrader5/)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
[](https://pepy.tech/projects/pythonmetatrader5)
|
|
8
|
+
[](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."""
|