pipsocket 0.1.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.
- pipsocket-0.1.0/.gitignore +10 -0
- pipsocket-0.1.0/CHANGELOG.md +40 -0
- pipsocket-0.1.0/LICENSE +21 -0
- pipsocket-0.1.0/PKG-INFO +358 -0
- pipsocket-0.1.0/README.md +327 -0
- pipsocket-0.1.0/conformance/README.md +52 -0
- pipsocket-0.1.0/conformance/call-cases.json +3046 -0
- pipsocket-0.1.0/conformance/extra-cases.json +135 -0
- pipsocket-0.1.0/openapi.json +7935 -0
- pipsocket-0.1.0/pyproject.toml +82 -0
- pipsocket-0.1.0/scripts/generate.py +351 -0
- pipsocket-0.1.0/scripts/spec_diff.py +99 -0
- pipsocket-0.1.0/src/pipsocket/__init__.py +68 -0
- pipsocket-0.1.0/src/pipsocket/_core.py +712 -0
- pipsocket-0.1.0/src/pipsocket/_generated/__init__.py +1 -0
- pipsocket-0.1.0/src/pipsocket/_generated/operations.py +35 -0
- pipsocket-0.1.0/src/pipsocket/_generated/tables.py +95 -0
- pipsocket-0.1.0/src/pipsocket/_generated/types.py +542 -0
- pipsocket-0.1.0/src/pipsocket/_http.py +235 -0
- pipsocket-0.1.0/src/pipsocket/_stream_core.py +916 -0
- pipsocket-0.1.0/src/pipsocket/_version.py +1 -0
- pipsocket-0.1.0/src/pipsocket/aio.py +702 -0
- pipsocket-0.1.0/src/pipsocket/client.py +680 -0
- pipsocket-0.1.0/src/pipsocket/errors.py +275 -0
- pipsocket-0.1.0/src/pipsocket/models.py +151 -0
- pipsocket-0.1.0/src/pipsocket/py.typed +0 -0
- pipsocket-0.1.0/src/pipsocket/stream.py +713 -0
- pipsocket-0.1.0/src/pipsocket/types.py +48 -0
- pipsocket-0.1.0/tests/__init__.py +0 -0
- pipsocket-0.1.0/tests/fake_stream.py +132 -0
- pipsocket-0.1.0/tests/fakes.py +179 -0
- pipsocket-0.1.0/tests/fixtures/spec/account.json +60 -0
- pipsocket-0.1.0/tests/fixtures/spec/cancel_order.json +261 -0
- pipsocket-0.1.0/tests/fixtures/spec/check_order.json +92 -0
- pipsocket-0.1.0/tests/fixtures/spec/close_by.json +261 -0
- pipsocket-0.1.0/tests/fixtures/spec/close_position.json +265 -0
- pipsocket-0.1.0/tests/fixtures/spec/engage_kill_switch.json +108 -0
- pipsocket-0.1.0/tests/fixtures/spec/history_deals.json +100 -0
- pipsocket-0.1.0/tests/fixtures/spec/history_orders.json +78 -0
- pipsocket-0.1.0/tests/fixtures/spec/kill_switch.json +46 -0
- pipsocket-0.1.0/tests/fixtures/spec/lift_kill_switch.json +47 -0
- pipsocket-0.1.0/tests/fixtures/spec/modify_order.json +271 -0
- pipsocket-0.1.0/tests/fixtures/spec/pending_orders.json +59 -0
- pipsocket-0.1.0/tests/fixtures/spec/place_order.json +267 -0
- pipsocket-0.1.0/tests/fixtures/spec/positions.json +59 -0
- pipsocket-0.1.0/tests/fixtures/spec/request.json +224 -0
- pipsocket-0.1.0/tests/fixtures/spec/set_position_levels.json +261 -0
- pipsocket-0.1.0/tests/fixtures/spec/symbol.json +95 -0
- pipsocket-0.1.0/tests/fixtures/spec/symbols.json +70 -0
- pipsocket-0.1.0/tests/fixtures/spec/tick.json +65 -0
- pipsocket-0.1.0/tests/test_client.py +315 -0
- pipsocket-0.1.0/tests/test_conformance.py +267 -0
- pipsocket-0.1.0/tests/test_contract.py +234 -0
- pipsocket-0.1.0/tests/test_core.py +509 -0
- pipsocket-0.1.0/tests/test_outcomes.py +849 -0
- pipsocket-0.1.0/tests/test_readme.py +131 -0
- pipsocket-0.1.0/tests/test_stream.py +777 -0
- pipsocket-0.1.0/tests/test_stream_machine.py +893 -0
- pipsocket-0.1.0/tests/test_trade_request.py +85 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Each release names the API document snapshot it was built from (`openapi.json`, with the
|
|
4
|
+
fingerprint `python scripts/spec_diff.py --fingerprint openapi.json` prints).
|
|
5
|
+
|
|
6
|
+
## 0.1.0 (10 Nov 2026)
|
|
7
|
+
|
|
8
|
+
The first release.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `Pipsocket` (sync) and `AsyncPipsocket` (async), with `api_key` (default `PIPSOCKET_KEY`),
|
|
13
|
+
`base_url`, `stream_url`, `timeout` (40 s per attempt), `max_retries` (2) and
|
|
14
|
+
`retry_deadline` (60 s).
|
|
15
|
+
- The read calls on `ps.account(id)`: `get`, `positions.list`, `orders.list`, `requests.get`,
|
|
16
|
+
`history.deals`, `history.orders`, `symbols.list`, `symbols.get`, `symbols.tick` and
|
|
17
|
+
`kill_switch.get`.
|
|
18
|
+
- The trade calls: `orders.place`, `orders.modify`, `orders.cancel`, `positions.close`,
|
|
19
|
+
`positions.close_by` and `positions.set_levels`. Each returns the recorded request as a
|
|
20
|
+
`TradeRequest` whatever its state, sends one idempotency key on every attempt, and raises
|
|
21
|
+
`OutcomeUnknown` (with the key) when the trade may have reached the host and no answer came back.
|
|
22
|
+
- `idempotency_key=` on the trade calls, checked before sending, and `pipsocket.new_key()`.
|
|
23
|
+
- `orders.check`, `kill_switch.engage` and `kill_switch.lift`.
|
|
24
|
+
- `requests.wait(id, timeout=30)`: looks a request up until it is final or the time is up.
|
|
25
|
+
- `acct.stream(ticks=[...])`, sync and async: the account's events, reconnected, with the tick set
|
|
26
|
+
sent again, keep-alive, and a `resync` event after every reconnection or gap carrying a new
|
|
27
|
+
snapshot and the history since the last checkpoint. A stream open on an account handle ends
|
|
28
|
+
that handle's `requests.wait()` as soon as the request settles. After `close()`, iteration
|
|
29
|
+
ends at once and events still queued are not delivered.
|
|
30
|
+
- `TradeRequest`, the type a recorded request comes back as, from a trade call, a lookup or a
|
|
31
|
+
stream event (`TradeRequest.from_event`).
|
|
32
|
+
- An exception for every error code the API answers with, and retries for the answers worth
|
|
33
|
+
repeating.
|
|
34
|
+
- Types for every answer, generated from the API's OpenAPI document (`pipsocket.types`).
|
|
35
|
+
- Dependencies: `httpx`, and `websockets` 16.1 or later (which drops credentials on a redirect
|
|
36
|
+
to another origin).
|
|
37
|
+
|
|
38
|
+
### Spec
|
|
39
|
+
|
|
40
|
+
- Built from the API document of 9 Nov 2026, fingerprint `ed112fbcea9b`.
|
pipsocket-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wilier Labs Pty Ltd (Pipsocket)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pipsocket-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pipsocket
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Pipsocket API: trade MetaTrader 5 accounts hosted by Pipsocket from Linux, macOS or Windows.
|
|
5
|
+
Project-URL: Homepage, https://pipsocket.com
|
|
6
|
+
Project-URL: Documentation, https://docs.pipsocket.com
|
|
7
|
+
Project-URL: Source, https://github.com/pipsocket/pipsocket-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/pipsocket/pipsocket-python/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/pipsocket/pipsocket-python/issues
|
|
10
|
+
Author-email: Pipsocket <support@pipsocket.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: algotrading,api-client,forex,pipsocket,trading,trading-api,websocket
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: httpx<1,>=0.27
|
|
29
|
+
Requires-Dist: websockets<18,>=16.1
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# pipsocket
|
|
33
|
+
|
|
34
|
+
Python client for the Pipsocket API: trade MetaTrader 5 accounts hosted by Pipsocket from Linux,
|
|
35
|
+
macOS or Windows.
|
|
36
|
+
|
|
37
|
+
## Why
|
|
38
|
+
|
|
39
|
+
The terminal runs at Pipsocket, so your code runs anywhere Python does: one `pip install`, no
|
|
40
|
+
terminal on your machine, and typed answers.
|
|
41
|
+
|
|
42
|
+
| | The MetaTrader5 package | pipsocket |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| Runs on | Windows only (its wheels on PyPI are all `win_amd64`, checked 9 Nov 2026) | Linux, macOS, Windows |
|
|
45
|
+
| Needs | An MT5 terminal running on the same machine | A Pipsocket account; the terminal is hosted |
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install pipsocket
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Python 3.10 or later. Its dependencies are [httpx](https://www.python-httpx.org) and
|
|
54
|
+
[websockets](https://websockets.readthedocs.io).
|
|
55
|
+
|
|
56
|
+
## Quickstart
|
|
57
|
+
|
|
58
|
+
Make your workspace's API key on the dashboard's **API key** page, where your accounts' ids are
|
|
59
|
+
listed too, and put the key in `PIPSOCKET_KEY`. Use a demo account while you try things.
|
|
60
|
+
|
|
61
|
+
Each symbol allows its own filling modes, and MT5 rejects an order with any other (retcode
|
|
62
|
+
10030), so read them, and the smallest volume, from the symbol rather than assuming one:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from pipsocket import Pipsocket
|
|
66
|
+
|
|
67
|
+
ps = Pipsocket() # reads PIPSOCKET_KEY
|
|
68
|
+
acct = ps.account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
|
|
69
|
+
|
|
70
|
+
print(acct.get()["balance"])
|
|
71
|
+
eurusd = acct.symbols.get("EURUSD")
|
|
72
|
+
filling = eurusd["filling_modes"][0] # one the symbol allows
|
|
73
|
+
volume = eurusd["volume_min"]
|
|
74
|
+
|
|
75
|
+
order = acct.orders.place(symbol="EURUSD", type="buy", volume=volume, type_filling=filling)
|
|
76
|
+
if order.outstanding: # pending or unknown: it may still execute
|
|
77
|
+
order = acct.requests.wait(order.id)
|
|
78
|
+
|
|
79
|
+
if order.accepted and order.result["deal"]: # MT5 took it and made a deal: filled
|
|
80
|
+
print("filled:", order.result["deal"], "at", order.result["price"])
|
|
81
|
+
# On a hedging account the position's ticket is the order's.
|
|
82
|
+
position = order.result["position"] or order.result["order"]
|
|
83
|
+
closed = acct.positions.close(position, volume=volume, type_filling=filling)
|
|
84
|
+
print("closed:", closed.state)
|
|
85
|
+
elif order.state == "rejected":
|
|
86
|
+
print("rejected:", order.retcode_name)
|
|
87
|
+
else:
|
|
88
|
+
print(order.state, order.message)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Answers are plain dicts, typed with `TypedDict`s from the API's OpenAPI document
|
|
92
|
+
(`pipsocket.types`), so they go straight into anything that takes dicts:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from pipsocket import Pipsocket
|
|
96
|
+
|
|
97
|
+
acct = Pipsocket().account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
|
|
98
|
+
|
|
99
|
+
# History ranges are the broker's server time in seconds, both ends included, not UTC: take
|
|
100
|
+
# them from the account's server_time (None until its terminal has reported).
|
|
101
|
+
now = acct.get()["server_time"]
|
|
102
|
+
if now is not None:
|
|
103
|
+
deals = acct.history.deals(now - 86400, now) # the last 24 hours
|
|
104
|
+
profit = sum(deal["profit"] for deal in deals)
|
|
105
|
+
print(len(deals), "deals,", profit)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Order outcomes
|
|
109
|
+
|
|
110
|
+
Each trade call (`orders.place`, `orders.modify`, `orders.cancel`, `positions.close`,
|
|
111
|
+
`positions.close_by`, `positions.set_levels`) is one MT5 trade request, sent once and recorded as
|
|
112
|
+
a request (`req_...`). The call returns that request as a `TradeRequest`, **whatever its state**:
|
|
113
|
+
a rejection is a result, not an exception. `acct.requests.get(id)` looks one up, and
|
|
114
|
+
`acct.requests.wait(id, timeout=30)` looks it up until it is final or the time is up, then
|
|
115
|
+
returns it as it stands; it never raises for "still pending" and never sends anything.
|
|
116
|
+
|
|
117
|
+
A `TradeRequest` has `id`, `state`, `sent`, `result`, `message`, `error`, `settlement`, the
|
|
118
|
+
whole body as `raw`, the `idempotency_key` it was sent with and the `http_status`, plus
|
|
119
|
+
`accepted`, `final`, `outstanding`, `retcode` and `retcode_name`.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from pipsocket import Pipsocket
|
|
123
|
+
|
|
124
|
+
acct = Pipsocket().account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
|
|
125
|
+
|
|
126
|
+
request = acct.requests.get("req_5f0c2a9e41d7b3a8c6e2f190")
|
|
127
|
+
match request.state:
|
|
128
|
+
case "completed" | "placed" | "partial":
|
|
129
|
+
if request.result["deal"]:
|
|
130
|
+
print("filled: deal", request.result["deal"])
|
|
131
|
+
else: # a pending order placed, or a change made: no deal
|
|
132
|
+
print("done, no deal: order", request.result["order"])
|
|
133
|
+
case "rejected":
|
|
134
|
+
print("MT5 rejected it:", request.retcode, request.retcode_name)
|
|
135
|
+
case "pending" | "unknown":
|
|
136
|
+
print("no outcome yet: it may still execute")
|
|
137
|
+
case _:
|
|
138
|
+
print(request.state, request.message)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| `state` | Meaning |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `completed` | MT5 completed the request (retcode 10009). Not necessarily a fill: see below. Final. |
|
|
144
|
+
| `placed` | MT5 placed it (retcode 10008), such as a pending order. Final. |
|
|
145
|
+
| `partial` | MT5 completed part of it (retcode 10010). Final. |
|
|
146
|
+
| `rejected` | MT5 rejected it: `retcode` says why. Final. |
|
|
147
|
+
| `pending` | Sent, no answer from the trade server yet. Never sent again. |
|
|
148
|
+
| `unknown` | No answer within 130 seconds: it may have executed. Settled by proof only. |
|
|
149
|
+
| `not_sent` | The terminal didn't send it: `result["error"]` says why. Final. |
|
|
150
|
+
| `not_executed` | Was `unknown`, settled as never executed (`settlement` says by whom). Final. |
|
|
151
|
+
| `executed` | Was `unknown`, settled as executed by the account's owner. Final. |
|
|
152
|
+
|
|
153
|
+
`completed` means MT5 completed the request, whatever it was: a market order filled, but also a
|
|
154
|
+
pending order placed, an order modified or cancelled, or stops set. So `accepted` (completed,
|
|
155
|
+
placed or partial) says MT5 took the request, not that anything filled. A fill shows as a deal:
|
|
156
|
+
`result["deal"]` is non-zero (and `sent["action"]` is `"deal"`). A pending order that was placed
|
|
157
|
+
has `result["order"]` set and `result["deal"]` 0.
|
|
158
|
+
|
|
159
|
+
`raise_for_state()` raises `TradeNotAccepted` for `rejected`, `not_sent` and `not_executed`, for
|
|
160
|
+
those who prefer exceptions. `orders.check(...)` is MT5's dry run of an order and sends nothing to
|
|
161
|
+
the broker. A state this SDK doesn't know yet comes through as its string,
|
|
162
|
+
neither accepted nor final. See [Errors and retcodes](https://docs.pipsocket.com/errors).
|
|
163
|
+
|
|
164
|
+
## Streaming
|
|
165
|
+
|
|
166
|
+
`acct.stream(...)` keeps the account's WebSocket open: requests settling, deals, prices and the
|
|
167
|
+
account as they happen, reconnected for you.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from pipsocket import Pipsocket, TradeRequest
|
|
171
|
+
|
|
172
|
+
acct = Pipsocket().account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
|
|
173
|
+
|
|
174
|
+
with acct.stream(ticks=["EURUSD"]) as events:
|
|
175
|
+
for event in events:
|
|
176
|
+
match event["type"]:
|
|
177
|
+
case "resync": # after every (re)connection or gap: a new baseline
|
|
178
|
+
print("resync:", event["reason"], len(event["positions"] or []), "positions")
|
|
179
|
+
case "tick":
|
|
180
|
+
print(event["symbol"], event["bid"], event["ask"])
|
|
181
|
+
case "request":
|
|
182
|
+
print("settled:", TradeRequest.from_event(event).state)
|
|
183
|
+
break
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Events are the API's messages as they are (see [the WebSocket stream](https://docs.pipsocket.com/websocket)),
|
|
187
|
+
plus one the SDK adds, `resync`. The helper sends the tick set again after every reconnect,
|
|
188
|
+
answers the keep-alive, and resyncs when the socket was closed, the stream said `lagged`, the
|
|
189
|
+
account's host came back, or your code fell behind (`reason`: `connected`, `reconnected`,
|
|
190
|
+
`lagged`, `host_back`, `overflow`).
|
|
191
|
+
|
|
192
|
+
What a `resync` gives:
|
|
193
|
+
|
|
194
|
+
- **A new baseline**: `account`, `positions` and `orders` read over REST after the stream was
|
|
195
|
+
live. Replace your own state with it. `positions` and `orders` are `None` when the terminal
|
|
196
|
+
isn't running, which is not the same as empty.
|
|
197
|
+
- **History since the last checkpoint**, best effort: `history` has the `deals` and `orders` from
|
|
198
|
+
the broker's server time `from` to `to`, or is `None` with `history_reason` saying why
|
|
199
|
+
(`no_checkpoint`, `account_changed`, `range_too_large` or `unavailable`).
|
|
200
|
+
- Every deal, request, transaction and status event from then on, delivered after it.
|
|
201
|
+
|
|
202
|
+
What it doesn't give: one moment in time (the reads happen one after another), an order between
|
|
203
|
+
events and the snapshot (events are notices, not deltas: never add them to the snapshot), or the
|
|
204
|
+
events from a gap. **Events that happened while the stream was away are not replayed**, `request`
|
|
205
|
+
events included: the resync's snapshot and `history` cover what changed, and for a request you
|
|
206
|
+
are waiting on, `acct.requests.wait(id)` looks it up over REST. Ticks and transaction events from
|
|
207
|
+
a gap are gone. Deduplicate deals and orders by ticket and requests by id, and read again when
|
|
208
|
+
exact state matters. `snapshot=False` sends `resync` without reading anything.
|
|
209
|
+
|
|
210
|
+
A resync's `history` starts 5 minutes before its checkpoint (the newest broker time the stream
|
|
211
|
+
delivered), so it can include deals from before you opened the stream; deals the stream already
|
|
212
|
+
delivered as events are left out of it.
|
|
213
|
+
|
|
214
|
+
The sync iterator (`for event in events`) blocks until an event arrives. To stop it, break out of
|
|
215
|
+
the loop, or call `events.close()` from another thread: iteration then ends at once, and events
|
|
216
|
+
still queued are not delivered. `AsyncPipsocket` has the same helper, with `async with` and
|
|
217
|
+
`async for`, where cancelling the task stops it too.
|
|
218
|
+
|
|
219
|
+
While a stream is open on an account handle, `acct.requests.wait(id)` ends as soon as the stream
|
|
220
|
+
sees the request settle.
|
|
221
|
+
|
|
222
|
+
## Errors, retries and idempotency keys
|
|
223
|
+
|
|
224
|
+
An exception means there is no recorded request in hand. Every exception derives from
|
|
225
|
+
`PipsocketError`. A refusal from the API is an `APIError` with `status`, `code` (the API's
|
|
226
|
+
`error`), `message`, `body` and `raw`, and a subclass for each kind: `AuthenticationError`,
|
|
227
|
+
`NotFound`, `InvalidRequest`, `SymbolBusy`, `KillSwitchEngaged`, `BillingHold`,
|
|
228
|
+
`AccountUnavailable`, `EngineBusy`, `Unavailable`, `TerminalTimeout`, `TerminalError`, `NoPrice`
|
|
229
|
+
and the rest. A network failure is an `APIConnectionError` (or `APITimeoutError`). None of
|
|
230
|
+
these, raised from a trade call, means anything was sent, with one exception:
|
|
231
|
+
|
|
232
|
+
**`OutcomeUnknown`** means the trade may have reached the account's host and no answer came
|
|
233
|
+
back. It may have executed. It carries the `idempotency_key` the call sent and the last error as
|
|
234
|
+
`cause`. Call again with the same key: you get the recorded request if there is one, and the
|
|
235
|
+
trade is sent if it never arrived. Or read positions and history first.
|
|
236
|
+
|
|
237
|
+
Every trade call sends an `Idempotency-Key`: a new one per call, the same one on every retry
|
|
238
|
+
inside the call. Calls that fail in a way worth repeating (the host busy or away, a dropped
|
|
239
|
+
connection, an answer that isn't the API's JSON, `symbol_busy` with `Retry-After`) are tried
|
|
240
|
+
again, twice by default, waiting 0.5 s and then 1 s (or what `Retry-After` says), and no new
|
|
241
|
+
attempt starts 60 seconds after the first: a market order sent minutes late is not what you
|
|
242
|
+
asked for. Refusals that won't change, such as a bad key or `kill_switch_engaged`, are raised at
|
|
243
|
+
once.
|
|
244
|
+
|
|
245
|
+
To survive a crash or a cancellation, make the key yourself and save it before sending. After a
|
|
246
|
+
restart, the same call with the same key returns the recorded request instead of trading again.
|
|
247
|
+
Make a new key for every new trade, and never build one from the order's fields (such as
|
|
248
|
+
"buy-eurusd"): the key is the whole match, so a second order with the same key returns the first
|
|
249
|
+
one and sends nothing.
|
|
250
|
+
|
|
251
|
+
A key protects a trade only while Pipsocket keeps its request: until 7 days after its outcome is
|
|
252
|
+
known. After that the same key sends a new trade. So resolve a journal's open entries promptly,
|
|
253
|
+
and never replay an old one blindly: check positions and history first.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
import json
|
|
257
|
+
import pathlib
|
|
258
|
+
|
|
259
|
+
import pipsocket
|
|
260
|
+
from pipsocket import OutcomeUnknown, Pipsocket
|
|
261
|
+
|
|
262
|
+
acct = Pipsocket().account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
|
|
263
|
+
journal = pathlib.Path("orders.jsonl")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def note(entry: dict[str, object]) -> None:
|
|
267
|
+
with journal.open("a") as file:
|
|
268
|
+
file.write(json.dumps(entry) + "\n")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
eurusd = acct.symbols.get("EURUSD")
|
|
272
|
+
filling, volume = eurusd["filling_modes"][0], eurusd["volume_min"]
|
|
273
|
+
key = pipsocket.new_key()
|
|
274
|
+
note({"key": key, "symbol": "EURUSD", "volume": volume}) # saved before it is sent
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
order = acct.orders.place(
|
|
278
|
+
symbol="EURUSD", type="buy", volume=volume, type_filling=filling, idempotency_key=key
|
|
279
|
+
)
|
|
280
|
+
except OutcomeUnknown as error:
|
|
281
|
+
# The entry stays open: after a restart, resolve it with the same key.
|
|
282
|
+
print("may have executed; call again with", error.idempotency_key)
|
|
283
|
+
else:
|
|
284
|
+
if order.final:
|
|
285
|
+
note({"key": key, "request": order.id, "state": order.state}) # resolved
|
|
286
|
+
else:
|
|
287
|
+
print("no outcome yet: wait for", order.id, "before closing the entry")
|
|
288
|
+
print(order.id, order.state)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Timeouts and retries can be set on the client, or per call:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from pipsocket import Pipsocket
|
|
295
|
+
|
|
296
|
+
ps = Pipsocket(timeout=20, max_retries=4, retry_deadline=30) # timeout is per attempt
|
|
297
|
+
acct = ps.account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
|
|
298
|
+
print(acct.kill_switch.get(max_retries=0)["engaged"])
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
The kill switch closes every position and cancels every pending order, and refuses new exposure
|
|
302
|
+
until it is lifted. Engaging it again retries whatever is left:
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from pipsocket import AccountUnavailable, Pipsocket
|
|
306
|
+
|
|
307
|
+
acct = Pipsocket().account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
|
|
308
|
+
try:
|
|
309
|
+
outcome = acct.kill_switch.engage(deviation=20)
|
|
310
|
+
print("flat:", outcome["flat"])
|
|
311
|
+
except AccountUnavailable as error:
|
|
312
|
+
print("engaged, but the terminal didn't answer:", error.kill_switch)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Sync and async
|
|
316
|
+
|
|
317
|
+
`Pipsocket` is synchronous; `AsyncPipsocket` has the same calls, awaited.
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
import asyncio
|
|
321
|
+
|
|
322
|
+
from pipsocket import AsyncPipsocket
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
async def main() -> None:
|
|
326
|
+
async with AsyncPipsocket() as ps:
|
|
327
|
+
acct = ps.account("acc_01m6tx3k9f2qw8r7bznd5yvh4c")
|
|
328
|
+
account, positions = await asyncio.gather(acct.get(), acct.positions.list())
|
|
329
|
+
print(account["equity"], len(positions))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
asyncio.run(main())
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Keep the key on your server: the API doesn't answer browsers, and the key is never logged or put
|
|
336
|
+
in an error message by this SDK.
|
|
337
|
+
|
|
338
|
+
## Links
|
|
339
|
+
|
|
340
|
+
- Docs: [Quickstart](https://docs.pipsocket.com/quickstart),
|
|
341
|
+
[API reference](https://docs.pipsocket.com/api/place-an-order),
|
|
342
|
+
[WebSocket stream](https://docs.pipsocket.com/websocket),
|
|
343
|
+
[Errors and retcodes](https://docs.pipsocket.com/errors)
|
|
344
|
+
- [MCP server](https://docs.pipsocket.com/mcp) for AI apps
|
|
345
|
+
- [Changelog](CHANGELOG.md)
|
|
346
|
+
|
|
347
|
+
## Versioning and support
|
|
348
|
+
|
|
349
|
+
Versions follow semver and are 0.x for now: until 1.0, a minor release (0.2, 0.3) may change
|
|
350
|
+
the API of this SDK, and the changelog says how. Report bugs in this SDK on
|
|
351
|
+
[GitHub issues](https://github.com/pipsocket/pipsocket-python/issues). Questions about your account
|
|
352
|
+
or billing go to [support@pipsocket.com](mailto:support@pipsocket.com).
|
|
353
|
+
|
|
354
|
+
## Licence
|
|
355
|
+
|
|
356
|
+
MIT. See [LICENSE](LICENSE).
|
|
357
|
+
|
|
358
|
+
MetaTrader is a trademark of MetaQuotes Ltd. Pipsocket is not affiliated with MetaQuotes.
|