fxsocket 0.2__tar.gz → 0.3.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.
- {fxsocket-0.2 → fxsocket-0.3.0}/PKG-INFO +38 -6
- {fxsocket-0.2 → fxsocket-0.3.0}/README.md +37 -5
- {fxsocket-0.2 → fxsocket-0.3.0}/src/fxsocket/__init__.py +10 -0
- fxsocket-0.3.0/src/fxsocket/_version.py +1 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/src/fxsocket/client.py +14 -6
- {fxsocket-0.2 → fxsocket-0.3.0}/src/fxsocket/enums.py +19 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/src/fxsocket/errors.py +22 -0
- fxsocket-0.3.0/src/fxsocket/management.py +264 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/src/fxsocket/models.py +61 -0
- fxsocket-0.3.0/tests/test_private_servers.py +181 -0
- fxsocket-0.2/src/fxsocket/_version.py +0 -1
- fxsocket-0.2/src/fxsocket/management.py +0 -118
- {fxsocket-0.2 → fxsocket-0.3.0}/.github/workflows/ci.yml +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/.github/workflows/publish.yml +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/.gitignore +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/LICENSE +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/examples/manage_accounts.py +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/examples/stream_quotes.py +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/examples/terminal_rest.py +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/pyproject.toml +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/src/fxsocket/_http.py +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/src/fxsocket/config.py +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/src/fxsocket/py.typed +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/src/fxsocket/terminal/__init__.py +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/src/fxsocket/terminal/client.py +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/src/fxsocket/terminal/stream.py +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/tests/test_errors.py +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/tests/test_management.py +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/tests/test_stream.py +0 -0
- {fxsocket-0.2 → fxsocket-0.3.0}/tests/test_terminal.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fxsocket
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python SDK for the FxSocket API — MT4/MT5 account management, trading, and real-time streaming.
|
|
5
5
|
Project-URL: Homepage, https://fxsocket.com
|
|
6
6
|
Project-URL: Documentation, https://api.fxsocket.com/v1/docs
|
|
@@ -40,6 +40,8 @@ interfaces.
|
|
|
40
40
|
## Features
|
|
41
41
|
|
|
42
42
|
- **Account management** — link, list, fetch, and disconnect MT4/MT5 accounts.
|
|
43
|
+
- **Private servers** — list your dedicated hosting servers and manage the
|
|
44
|
+
accounts on them.
|
|
43
45
|
- **Trading** — market & pending orders, modify, close, plus margin/profit calculators.
|
|
44
46
|
- **Market data** — quotes, symbol specifications, OHLC history, account state & info.
|
|
45
47
|
- **Live streaming** — ticks, bars, account, positions, trades, and terminal status
|
|
@@ -238,11 +240,41 @@ except AccountCapError as e:
|
|
|
238
240
|
|
|
239
241
|
## Private hosting
|
|
240
242
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
Dedicated private servers are managed through `client.private_servers`:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
import time
|
|
247
|
+
|
|
248
|
+
from fxsocket import Client, PrivateAccountStatus, SlotsFullError
|
|
249
|
+
|
|
250
|
+
with Client(api_key="fxs_live_...", verify_terminal_tls=False) as fx:
|
|
251
|
+
[server] = fx.private_servers.list()
|
|
252
|
+
print(server.name, server.status, f"{server.used_slots}/{server.purchased_slots}")
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
account = fx.private_servers.add_account(
|
|
256
|
+
server, server="ICMarkets-Demo", login=1150125, password="..."
|
|
257
|
+
)
|
|
258
|
+
except SlotsFullError as err:
|
|
259
|
+
print(f"Server full ({err.used}/{err.cap}) — raise the limit in the dashboard.")
|
|
260
|
+
|
|
261
|
+
# Poll until the on-server agent has the terminal up, then trade as usual.
|
|
262
|
+
while True:
|
|
263
|
+
server = fx.private_servers.get(server)
|
|
264
|
+
account = next(a for a in server.accounts if a.id == account.id)
|
|
265
|
+
if account.status == PrivateAccountStatus.READY:
|
|
266
|
+
break
|
|
267
|
+
time.sleep(5)
|
|
268
|
+
|
|
269
|
+
print(fx.terminal(account).account_summary())
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Accounts on a private server are traded and streamed exactly like
|
|
273
|
+
shared-cluster accounts — their `rest_url` / `ws_url` simply point at the
|
|
274
|
+
server's dedicated IP. The server presents a self-signed certificate, so reach
|
|
275
|
+
it with `Client(..., verify_terminal_tls=False)` (or supply a pinned CA).
|
|
276
|
+
*Purchasing* a server, canceling, and slot changes happen in the dashboard;
|
|
277
|
+
the API deliberately exposes no billing operations.
|
|
246
278
|
|
|
247
279
|
## Timestamps
|
|
248
280
|
|
|
@@ -13,6 +13,8 @@ interfaces.
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
15
|
- **Account management** — link, list, fetch, and disconnect MT4/MT5 accounts.
|
|
16
|
+
- **Private servers** — list your dedicated hosting servers and manage the
|
|
17
|
+
accounts on them.
|
|
16
18
|
- **Trading** — market & pending orders, modify, close, plus margin/profit calculators.
|
|
17
19
|
- **Market data** — quotes, symbol specifications, OHLC history, account state & info.
|
|
18
20
|
- **Live streaming** — ticks, bars, account, positions, trades, and terminal status
|
|
@@ -211,11 +213,41 @@ except AccountCapError as e:
|
|
|
211
213
|
|
|
212
214
|
## Private hosting
|
|
213
215
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
Dedicated private servers are managed through `client.private_servers`:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
import time
|
|
220
|
+
|
|
221
|
+
from fxsocket import Client, PrivateAccountStatus, SlotsFullError
|
|
222
|
+
|
|
223
|
+
with Client(api_key="fxs_live_...", verify_terminal_tls=False) as fx:
|
|
224
|
+
[server] = fx.private_servers.list()
|
|
225
|
+
print(server.name, server.status, f"{server.used_slots}/{server.purchased_slots}")
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
account = fx.private_servers.add_account(
|
|
229
|
+
server, server="ICMarkets-Demo", login=1150125, password="..."
|
|
230
|
+
)
|
|
231
|
+
except SlotsFullError as err:
|
|
232
|
+
print(f"Server full ({err.used}/{err.cap}) — raise the limit in the dashboard.")
|
|
233
|
+
|
|
234
|
+
# Poll until the on-server agent has the terminal up, then trade as usual.
|
|
235
|
+
while True:
|
|
236
|
+
server = fx.private_servers.get(server)
|
|
237
|
+
account = next(a for a in server.accounts if a.id == account.id)
|
|
238
|
+
if account.status == PrivateAccountStatus.READY:
|
|
239
|
+
break
|
|
240
|
+
time.sleep(5)
|
|
241
|
+
|
|
242
|
+
print(fx.terminal(account).account_summary())
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Accounts on a private server are traded and streamed exactly like
|
|
246
|
+
shared-cluster accounts — their `rest_url` / `ws_url` simply point at the
|
|
247
|
+
server's dedicated IP. The server presents a self-signed certificate, so reach
|
|
248
|
+
it with `Client(..., verify_terminal_tls=False)` (or supply a pinned CA).
|
|
249
|
+
*Purchasing* a server, canceling, and slot changes happen in the dashboard;
|
|
250
|
+
the API deliberately exposes no billing operations.
|
|
219
251
|
|
|
220
252
|
## Timestamps
|
|
221
253
|
|
|
@@ -16,6 +16,8 @@ from .enums import (
|
|
|
16
16
|
OrderOperation,
|
|
17
17
|
OrderOutcome,
|
|
18
18
|
Platform,
|
|
19
|
+
PrivateAccountStatus,
|
|
20
|
+
PrivateServerStatus,
|
|
19
21
|
Timeframe,
|
|
20
22
|
TradingStatus,
|
|
21
23
|
)
|
|
@@ -28,6 +30,7 @@ from .errors import (
|
|
|
28
30
|
NoSubscriptionError,
|
|
29
31
|
NotFoundError,
|
|
30
32
|
RateLimitError,
|
|
33
|
+
SlotsFullError,
|
|
31
34
|
StreamError,
|
|
32
35
|
TerminalNotReadyError,
|
|
33
36
|
TerminalTimeoutError,
|
|
@@ -46,6 +49,8 @@ from .models import (
|
|
|
46
49
|
OpenedOrder,
|
|
47
50
|
OrderResult,
|
|
48
51
|
PositionTrade,
|
|
52
|
+
PrivateServer,
|
|
53
|
+
PrivateServerAccount,
|
|
49
54
|
ProfitCalc,
|
|
50
55
|
Quote,
|
|
51
56
|
ServerTimezone,
|
|
@@ -97,6 +102,8 @@ __all__ = [
|
|
|
97
102
|
"TradeEventData",
|
|
98
103
|
"TerminalStatusData",
|
|
99
104
|
"Account",
|
|
105
|
+
"PrivateServer",
|
|
106
|
+
"PrivateServerAccount",
|
|
100
107
|
"AccountSummary",
|
|
101
108
|
"AccountInfo",
|
|
102
109
|
"OpenedOrder",
|
|
@@ -112,6 +119,8 @@ __all__ = [
|
|
|
112
119
|
"Health",
|
|
113
120
|
"HealthChecks",
|
|
114
121
|
"Platform",
|
|
122
|
+
"PrivateAccountStatus",
|
|
123
|
+
"PrivateServerStatus",
|
|
115
124
|
"TradingStatus",
|
|
116
125
|
"OrderOperation",
|
|
117
126
|
"OrderOutcome",
|
|
@@ -128,6 +137,7 @@ __all__ = [
|
|
|
128
137
|
"AccountCapError",
|
|
129
138
|
"NoSubscriptionError",
|
|
130
139
|
"DuplicateAccountError",
|
|
140
|
+
"SlotsFullError",
|
|
131
141
|
"ConnectFailedError",
|
|
132
142
|
"TerminalNotReadyError",
|
|
133
143
|
"TerminalTimeoutError",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -15,8 +15,13 @@ import httpx
|
|
|
15
15
|
from ._http import AsyncHTTP, SyncHTTP, auth_headers
|
|
16
16
|
from .config import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, ENV_API_KEY
|
|
17
17
|
from .errors import AuthError, TerminalNotReadyError
|
|
18
|
-
from .management import
|
|
19
|
-
|
|
18
|
+
from .management import (
|
|
19
|
+
Accounts,
|
|
20
|
+
AsyncAccounts,
|
|
21
|
+
AsyncPrivateServers,
|
|
22
|
+
PrivateServers,
|
|
23
|
+
)
|
|
24
|
+
from .models import Account, PrivateServerAccount
|
|
20
25
|
from .terminal.client import AsyncTerminalClient, TerminalClient
|
|
21
26
|
from .terminal.stream import AsyncStream, Stream
|
|
22
27
|
|
|
@@ -63,11 +68,13 @@ class Client:
|
|
|
63
68
|
)
|
|
64
69
|
#: Account management (the v1 API).
|
|
65
70
|
self.accounts = Accounts(SyncHTTP(self._http_client))
|
|
71
|
+
#: Private hosting servers (the v1 API).
|
|
72
|
+
self.private_servers = PrivateServers(SyncHTTP(self._http_client))
|
|
66
73
|
self._terminals: dict[tuple[str, str], TerminalClient] = {}
|
|
67
74
|
|
|
68
75
|
def terminal(
|
|
69
76
|
self,
|
|
70
|
-
account: Account,
|
|
77
|
+
account: Account | PrivateServerAccount,
|
|
71
78
|
*,
|
|
72
79
|
verify: bool | None = None,
|
|
73
80
|
timeout: float | None = None,
|
|
@@ -100,7 +107,7 @@ class Client:
|
|
|
100
107
|
|
|
101
108
|
def stream(
|
|
102
109
|
self,
|
|
103
|
-
account: Account,
|
|
110
|
+
account: Account | PrivateServerAccount,
|
|
104
111
|
*,
|
|
105
112
|
verify: bool | None = None,
|
|
106
113
|
auto_reconnect: bool = True,
|
|
@@ -178,11 +185,12 @@ class AsyncClient:
|
|
|
178
185
|
timeout=timeout,
|
|
179
186
|
)
|
|
180
187
|
self.accounts = AsyncAccounts(AsyncHTTP(self._http_client))
|
|
188
|
+
self.private_servers = AsyncPrivateServers(AsyncHTTP(self._http_client))
|
|
181
189
|
self._terminals: dict[tuple[str, str], AsyncTerminalClient] = {}
|
|
182
190
|
|
|
183
191
|
def terminal(
|
|
184
192
|
self,
|
|
185
|
-
account: Account,
|
|
193
|
+
account: Account | PrivateServerAccount,
|
|
186
194
|
*,
|
|
187
195
|
verify: bool | None = None,
|
|
188
196
|
timeout: float | None = None,
|
|
@@ -212,7 +220,7 @@ class AsyncClient:
|
|
|
212
220
|
|
|
213
221
|
def stream(
|
|
214
222
|
self,
|
|
215
|
-
account: Account,
|
|
223
|
+
account: Account | PrivateServerAccount,
|
|
216
224
|
*,
|
|
217
225
|
verify: bool | None = None,
|
|
218
226
|
auto_reconnect: bool = True,
|
|
@@ -76,6 +76,25 @@ class OrderOutcome(str, Enum):
|
|
|
76
76
|
REJECTED = "rejected" #: anything else — inspect ``retcode`` / ``comment``
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
class PrivateServerStatus(str, Enum):
|
|
80
|
+
"""Lifecycle of a private hosting server (v1 ``status``)."""
|
|
81
|
+
|
|
82
|
+
PENDING_PAYMENT = "pending_payment"
|
|
83
|
+
PROVISIONING = "provisioning"
|
|
84
|
+
READY = "ready"
|
|
85
|
+
RESIZING = "resizing"
|
|
86
|
+
EXPIRED = "expired"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class PrivateAccountStatus(str, Enum):
|
|
90
|
+
"""Lifecycle of an account living on a private server (v1 ``status``)."""
|
|
91
|
+
|
|
92
|
+
PROVISIONING = "provisioning"
|
|
93
|
+
READY = "ready"
|
|
94
|
+
ERROR = "error"
|
|
95
|
+
EXPIRED = "expired"
|
|
96
|
+
|
|
97
|
+
|
|
79
98
|
class OrderKind(str, Enum):
|
|
80
99
|
"""Whether an opened row is a live position or a resting pending order."""
|
|
81
100
|
|
|
@@ -74,6 +74,24 @@ class DuplicateAccountError(FxSocketError):
|
|
|
74
74
|
"""This account is already linked (HTTP 409)."""
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
class SlotsFullError(FxSocketError):
|
|
78
|
+
"""Every purchased slot on the private server is taken (HTTP 409
|
|
79
|
+
``slots_full``). Raise the server's limit from the dashboard."""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
message: str,
|
|
84
|
+
*,
|
|
85
|
+
used: int | None = None,
|
|
86
|
+
cap: int | None = None,
|
|
87
|
+
**kw: Any,
|
|
88
|
+
):
|
|
89
|
+
super().__init__(message, **kw)
|
|
90
|
+
self.used = used
|
|
91
|
+
self.cap = cap
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
77
95
|
class ConnectFailedError(FxSocketError):
|
|
78
96
|
"""The broker rejected the login during account creation (HTTP 400).
|
|
79
97
|
|
|
@@ -145,6 +163,10 @@ def error_from_response(resp: httpx.Response) -> FxSocketError:
|
|
|
145
163
|
if status == 404:
|
|
146
164
|
return NotFoundError(message, **common)
|
|
147
165
|
if status == 409:
|
|
166
|
+
if code == "slots_full" and isinstance(body, dict):
|
|
167
|
+
return SlotsFullError(
|
|
168
|
+
message, used=body.get("used"), cap=body.get("cap"), **common
|
|
169
|
+
)
|
|
148
170
|
return DuplicateAccountError(message, **common)
|
|
149
171
|
if status == 402:
|
|
150
172
|
if code == "account_cap_reached" and isinstance(body, dict):
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Account management — the public v1 API (``/v1/accounts``).
|
|
2
|
+
|
|
3
|
+
Accessible as ``client.accounts`` on both :class:`~fxsocket.Client` and
|
|
4
|
+
:class:`~fxsocket.AsyncClient`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ._http import AsyncHTTP, SyncHTTP
|
|
10
|
+
from .enums import Platform
|
|
11
|
+
from .models import Account, PrivateServer, PrivateServerAccount
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def account_id_of(account: Account | str) -> str:
|
|
15
|
+
"""Accept either an :class:`Account` or a bare id string."""
|
|
16
|
+
return account.id if isinstance(account, Account) else str(account)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _create_payload(
|
|
20
|
+
*,
|
|
21
|
+
server: str,
|
|
22
|
+
login: int,
|
|
23
|
+
password: str,
|
|
24
|
+
platform: Platform | str,
|
|
25
|
+
nickname: str,
|
|
26
|
+
) -> dict[str, object]:
|
|
27
|
+
return {
|
|
28
|
+
"platform": Platform(platform).value,
|
|
29
|
+
"server": server,
|
|
30
|
+
"login": login,
|
|
31
|
+
"password": password,
|
|
32
|
+
"nickname": nickname,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Accounts:
|
|
37
|
+
"""Synchronous account operations."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, http: SyncHTTP) -> None:
|
|
40
|
+
self._http = http
|
|
41
|
+
|
|
42
|
+
def list(self) -> list[Account]:
|
|
43
|
+
"""List every account owned by the authenticated user."""
|
|
44
|
+
data = self._http.request("GET", "/accounts")
|
|
45
|
+
return [Account.model_validate(row) for row in data]
|
|
46
|
+
|
|
47
|
+
def get(self, account: Account | str) -> Account:
|
|
48
|
+
"""Fetch one account by id (use this to poll connection status)."""
|
|
49
|
+
data = self._http.request("GET", f"/accounts/{account_id_of(account)}")
|
|
50
|
+
return Account.model_validate(data)
|
|
51
|
+
|
|
52
|
+
def create(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
server: str,
|
|
56
|
+
login: int,
|
|
57
|
+
password: str,
|
|
58
|
+
platform: Platform | str = Platform.MT5,
|
|
59
|
+
nickname: str = "",
|
|
60
|
+
) -> Account:
|
|
61
|
+
"""Link (connect) a new MT4/MT5 account. Returns it in ``connecting``
|
|
62
|
+
state when terminal pods are enabled, else ``connected``."""
|
|
63
|
+
data = self._http.request(
|
|
64
|
+
"POST",
|
|
65
|
+
"/accounts",
|
|
66
|
+
json=_create_payload(
|
|
67
|
+
server=server,
|
|
68
|
+
login=login,
|
|
69
|
+
password=password,
|
|
70
|
+
platform=platform,
|
|
71
|
+
nickname=nickname,
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
return Account.model_validate(data)
|
|
75
|
+
|
|
76
|
+
def delete(self, account: Account | str) -> None:
|
|
77
|
+
"""Unlink (disconnect) an account and tear down its terminal."""
|
|
78
|
+
self._http.request("DELETE", f"/accounts/{account_id_of(account)}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AsyncAccounts:
|
|
82
|
+
"""Asynchronous mirror of :class:`Accounts`."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, http: AsyncHTTP) -> None:
|
|
85
|
+
self._http = http
|
|
86
|
+
|
|
87
|
+
async def list(self) -> list[Account]:
|
|
88
|
+
data = await self._http.request("GET", "/accounts")
|
|
89
|
+
return [Account.model_validate(row) for row in data]
|
|
90
|
+
|
|
91
|
+
async def get(self, account: Account | str) -> Account:
|
|
92
|
+
data = await self._http.request("GET", f"/accounts/{account_id_of(account)}")
|
|
93
|
+
return Account.model_validate(data)
|
|
94
|
+
|
|
95
|
+
async def create(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
server: str,
|
|
99
|
+
login: int,
|
|
100
|
+
password: str,
|
|
101
|
+
platform: Platform | str = Platform.MT5,
|
|
102
|
+
nickname: str = "",
|
|
103
|
+
) -> Account:
|
|
104
|
+
data = await self._http.request(
|
|
105
|
+
"POST",
|
|
106
|
+
"/accounts",
|
|
107
|
+
json=_create_payload(
|
|
108
|
+
server=server,
|
|
109
|
+
login=login,
|
|
110
|
+
password=password,
|
|
111
|
+
platform=platform,
|
|
112
|
+
nickname=nickname,
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
return Account.model_validate(data)
|
|
116
|
+
|
|
117
|
+
async def delete(self, account: Account | str) -> None:
|
|
118
|
+
await self._http.request("DELETE", f"/accounts/{account_id_of(account)}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# --------------------------------------------------------------------------- #
|
|
122
|
+
# Private servers (``/v1/private-servers``)
|
|
123
|
+
# --------------------------------------------------------------------------- #
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def private_server_id_of(server: PrivateServer | str) -> str:
|
|
127
|
+
"""Accept either a :class:`PrivateServer` or a bare id string."""
|
|
128
|
+
return server.id if isinstance(server, PrivateServer) else str(server)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _private_account_payload(
|
|
132
|
+
*,
|
|
133
|
+
server: str,
|
|
134
|
+
login: int,
|
|
135
|
+
password: str,
|
|
136
|
+
platform: Platform | str,
|
|
137
|
+
nickname: str,
|
|
138
|
+
trade_ea_symbol: str,
|
|
139
|
+
) -> dict[str, object]:
|
|
140
|
+
return {
|
|
141
|
+
"platform": Platform(platform).value,
|
|
142
|
+
"server": server,
|
|
143
|
+
"login": login,
|
|
144
|
+
"password": password,
|
|
145
|
+
"nickname": nickname,
|
|
146
|
+
"trade_ea_symbol": trade_ea_symbol,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class PrivateServers:
|
|
151
|
+
"""Synchronous private-server operations.
|
|
152
|
+
|
|
153
|
+
Read + on-server account management only: purchasing a server,
|
|
154
|
+
canceling, and slot changes happen in the dashboard.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, http: SyncHTTP) -> None:
|
|
158
|
+
self._http = http
|
|
159
|
+
|
|
160
|
+
def list(self) -> list[PrivateServer]:
|
|
161
|
+
"""List every private server owned by the authenticated user."""
|
|
162
|
+
data = self._http.request("GET", "/private-servers")
|
|
163
|
+
return [PrivateServer.model_validate(row) for row in data]
|
|
164
|
+
|
|
165
|
+
def get(self, server: PrivateServer | str) -> PrivateServer:
|
|
166
|
+
"""Fetch one server by id (use this to poll account readiness)."""
|
|
167
|
+
data = self._http.request(
|
|
168
|
+
"GET", f"/private-servers/{private_server_id_of(server)}"
|
|
169
|
+
)
|
|
170
|
+
return PrivateServer.model_validate(data)
|
|
171
|
+
|
|
172
|
+
def add_account(
|
|
173
|
+
self,
|
|
174
|
+
private_server: PrivateServer | str,
|
|
175
|
+
*,
|
|
176
|
+
server: str,
|
|
177
|
+
login: int,
|
|
178
|
+
password: str,
|
|
179
|
+
platform: Platform | str = Platform.MT5,
|
|
180
|
+
nickname: str = "",
|
|
181
|
+
trade_ea_symbol: str = "",
|
|
182
|
+
) -> PrivateServerAccount:
|
|
183
|
+
"""Connect an MT4/MT5 account onto the server.
|
|
184
|
+
|
|
185
|
+
The on-server agent brings the terminal up asynchronously — poll
|
|
186
|
+
:meth:`get` until the account's ``status`` reaches ``ready``.
|
|
187
|
+
Raises :class:`~fxsocket.SlotsFullError` when every purchased slot
|
|
188
|
+
is taken and :class:`~fxsocket.DuplicateAccountError` when the
|
|
189
|
+
account is already linked.
|
|
190
|
+
"""
|
|
191
|
+
data = self._http.request(
|
|
192
|
+
"POST",
|
|
193
|
+
f"/private-servers/{private_server_id_of(private_server)}/accounts",
|
|
194
|
+
json=_private_account_payload(
|
|
195
|
+
server=server,
|
|
196
|
+
login=login,
|
|
197
|
+
password=password,
|
|
198
|
+
platform=platform,
|
|
199
|
+
nickname=nickname,
|
|
200
|
+
trade_ea_symbol=trade_ea_symbol,
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
return PrivateServerAccount.model_validate(data)
|
|
204
|
+
|
|
205
|
+
def remove_account(
|
|
206
|
+
self,
|
|
207
|
+
private_server: PrivateServer | str,
|
|
208
|
+
account: PrivateServerAccount | str,
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Detach an account from the server, freeing its slot."""
|
|
211
|
+
sid = private_server_id_of(private_server)
|
|
212
|
+
aid = account.id if isinstance(account, PrivateServerAccount) else str(account)
|
|
213
|
+
self._http.request("DELETE", f"/private-servers/{sid}/accounts/{aid}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class AsyncPrivateServers:
|
|
217
|
+
"""Asynchronous mirror of :class:`PrivateServers`."""
|
|
218
|
+
|
|
219
|
+
def __init__(self, http: AsyncHTTP) -> None:
|
|
220
|
+
self._http = http
|
|
221
|
+
|
|
222
|
+
async def list(self) -> list[PrivateServer]:
|
|
223
|
+
data = await self._http.request("GET", "/private-servers")
|
|
224
|
+
return [PrivateServer.model_validate(row) for row in data]
|
|
225
|
+
|
|
226
|
+
async def get(self, server: PrivateServer | str) -> PrivateServer:
|
|
227
|
+
data = await self._http.request(
|
|
228
|
+
"GET", f"/private-servers/{private_server_id_of(server)}"
|
|
229
|
+
)
|
|
230
|
+
return PrivateServer.model_validate(data)
|
|
231
|
+
|
|
232
|
+
async def add_account(
|
|
233
|
+
self,
|
|
234
|
+
private_server: PrivateServer | str,
|
|
235
|
+
*,
|
|
236
|
+
server: str,
|
|
237
|
+
login: int,
|
|
238
|
+
password: str,
|
|
239
|
+
platform: Platform | str = Platform.MT5,
|
|
240
|
+
nickname: str = "",
|
|
241
|
+
trade_ea_symbol: str = "",
|
|
242
|
+
) -> PrivateServerAccount:
|
|
243
|
+
data = await self._http.request(
|
|
244
|
+
"POST",
|
|
245
|
+
f"/private-servers/{private_server_id_of(private_server)}/accounts",
|
|
246
|
+
json=_private_account_payload(
|
|
247
|
+
server=server,
|
|
248
|
+
login=login,
|
|
249
|
+
password=password,
|
|
250
|
+
platform=platform,
|
|
251
|
+
nickname=nickname,
|
|
252
|
+
trade_ea_symbol=trade_ea_symbol,
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
return PrivateServerAccount.model_validate(data)
|
|
256
|
+
|
|
257
|
+
async def remove_account(
|
|
258
|
+
self,
|
|
259
|
+
private_server: PrivateServer | str,
|
|
260
|
+
account: PrivateServerAccount | str,
|
|
261
|
+
) -> None:
|
|
262
|
+
sid = private_server_id_of(private_server)
|
|
263
|
+
aid = account.id if isinstance(account, PrivateServerAccount) else str(account)
|
|
264
|
+
await self._http.request("DELETE", f"/private-servers/{sid}/accounts/{aid}")
|
|
@@ -70,6 +70,67 @@ class Account(BaseModel):
|
|
|
70
70
|
return bool(self.rest_url)
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
class PrivateServerAccount(BaseModel):
|
|
74
|
+
"""An MT4/MT5 account living on a private server (v1 API).
|
|
75
|
+
|
|
76
|
+
``status`` is the private-hosting lifecycle — compare against
|
|
77
|
+
:class:`fxsocket.PrivateAccountStatus` (provisioning / ready / error /
|
|
78
|
+
expired). ``rest_url`` / ``ws_url`` are the account's terminal API on
|
|
79
|
+
the server's dedicated IP. Private servers use a self-signed TLS
|
|
80
|
+
certificate, so pass ``verify=False`` (or construct the client with
|
|
81
|
+
``verify_terminal_tls=False``) when calling ``client.terminal(...)``.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
|
85
|
+
|
|
86
|
+
id: str
|
|
87
|
+
nickname: str = ""
|
|
88
|
+
platform: Platform
|
|
89
|
+
server: str
|
|
90
|
+
login: int
|
|
91
|
+
status: str
|
|
92
|
+
rest_url: str = ""
|
|
93
|
+
ws_url: str = ""
|
|
94
|
+
trade_ea_symbol: str = ""
|
|
95
|
+
created_at: datetime
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def has_terminal(self) -> bool:
|
|
99
|
+
"""True when this account exposes a reachable terminal API."""
|
|
100
|
+
return bool(self.rest_url)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class PrivateServer(BaseModel):
|
|
104
|
+
"""A dedicated private hosting server (v1 API).
|
|
105
|
+
|
|
106
|
+
``status`` is the server lifecycle — compare against
|
|
107
|
+
:class:`fxsocket.PrivateServerStatus`. ``purchased_slots`` is the paid
|
|
108
|
+
limit; ``used_slots`` how many accounts currently live on the server.
|
|
109
|
+
Purchasing, canceling and slot changes happen in the dashboard, not
|
|
110
|
+
the API.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
|
114
|
+
|
|
115
|
+
id: str
|
|
116
|
+
name: str = ""
|
|
117
|
+
status: str
|
|
118
|
+
region: str = ""
|
|
119
|
+
ip: str = ""
|
|
120
|
+
purchased_slots: int = 0
|
|
121
|
+
used_slots: int = 0
|
|
122
|
+
period_end: datetime | None = None
|
|
123
|
+
accounts: list[PrivateServerAccount] = []
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def is_ready(self) -> bool:
|
|
127
|
+
return self.status == "ready"
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def free_slots(self) -> int:
|
|
131
|
+
return max(self.purchased_slots - self.used_slots, 0)
|
|
132
|
+
|
|
133
|
+
|
|
73
134
|
# --------------------------------------------------------------------------- #
|
|
74
135
|
# Terminal — account state
|
|
75
136
|
# --------------------------------------------------------------------------- #
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Tests for the private-server management client (sync + async)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
import respx
|
|
8
|
+
|
|
9
|
+
from fxsocket import (
|
|
10
|
+
AsyncClient,
|
|
11
|
+
Client,
|
|
12
|
+
DuplicateAccountError,
|
|
13
|
+
PrivateAccountStatus,
|
|
14
|
+
PrivateServerStatus,
|
|
15
|
+
SlotsFullError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
BASE = "https://api.fxsocket.com/v1"
|
|
19
|
+
|
|
20
|
+
SERVER_ID = "ecf2fa95-4177-4402-8a00-dea33ae0e79a"
|
|
21
|
+
|
|
22
|
+
SERVER_ACCOUNT = {
|
|
23
|
+
"id": "22222222-2222-2222-2222-222222222222",
|
|
24
|
+
"nickname": "prop-1",
|
|
25
|
+
"platform": "mt5",
|
|
26
|
+
"server": "ICMarkets-Demo",
|
|
27
|
+
"login": 7001,
|
|
28
|
+
"status": "ready",
|
|
29
|
+
"rest_url": f"https://159.223.244.125/22222222-2222-2222-2222-222222222222",
|
|
30
|
+
"ws_url": f"wss://159.223.244.125/22222222-2222-2222-2222-222222222222/ws",
|
|
31
|
+
"trade_ea_symbol": "",
|
|
32
|
+
"created_at": "2026-07-16T08:00:00Z",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
SERVER = {
|
|
36
|
+
"id": SERVER_ID,
|
|
37
|
+
"name": "My Prop Guard",
|
|
38
|
+
"status": "ready",
|
|
39
|
+
"region": "lon1",
|
|
40
|
+
"ip": "159.223.244.125",
|
|
41
|
+
"purchased_slots": 2,
|
|
42
|
+
"used_slots": 1,
|
|
43
|
+
"period_end": "2026-08-16T07:01:08Z",
|
|
44
|
+
"accounts": [SERVER_ACCOUNT],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _client() -> Client:
|
|
49
|
+
return Client(api_key="fxs_live_test")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@respx.mock
|
|
53
|
+
def test_list_servers_parses_models() -> None:
|
|
54
|
+
respx.get(f"{BASE}/private-servers").mock(
|
|
55
|
+
return_value=httpx.Response(200, json=[SERVER])
|
|
56
|
+
)
|
|
57
|
+
with _client() as fx:
|
|
58
|
+
[server] = fx.private_servers.list()
|
|
59
|
+
assert server.name == "My Prop Guard"
|
|
60
|
+
assert server.status == PrivateServerStatus.READY
|
|
61
|
+
assert server.is_ready
|
|
62
|
+
assert server.ip == "159.223.244.125"
|
|
63
|
+
assert server.free_slots == 1
|
|
64
|
+
[account] = server.accounts
|
|
65
|
+
assert account.status == PrivateAccountStatus.READY
|
|
66
|
+
assert account.has_terminal
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@respx.mock
|
|
70
|
+
def test_get_accepts_model_or_id() -> None:
|
|
71
|
+
route = respx.get(f"{BASE}/private-servers/{SERVER_ID}").mock(
|
|
72
|
+
return_value=httpx.Response(200, json=SERVER)
|
|
73
|
+
)
|
|
74
|
+
with _client() as fx:
|
|
75
|
+
by_id = fx.private_servers.get(SERVER_ID)
|
|
76
|
+
by_model = fx.private_servers.get(by_id)
|
|
77
|
+
assert route.call_count == 2
|
|
78
|
+
assert by_model.id == SERVER_ID
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@respx.mock
|
|
82
|
+
def test_add_account_payload_and_model() -> None:
|
|
83
|
+
route = respx.post(f"{BASE}/private-servers/{SERVER_ID}/accounts").mock(
|
|
84
|
+
return_value=httpx.Response(201, json=SERVER_ACCOUNT)
|
|
85
|
+
)
|
|
86
|
+
with _client() as fx:
|
|
87
|
+
account = fx.private_servers.add_account(
|
|
88
|
+
SERVER_ID,
|
|
89
|
+
server="ICMarkets-Demo",
|
|
90
|
+
login=7001,
|
|
91
|
+
password="pw",
|
|
92
|
+
nickname="prop-1",
|
|
93
|
+
)
|
|
94
|
+
import json
|
|
95
|
+
|
|
96
|
+
sent = json.loads(route.calls.last.request.content)
|
|
97
|
+
assert sent == {
|
|
98
|
+
"platform": "mt5",
|
|
99
|
+
"server": "ICMarkets-Demo",
|
|
100
|
+
"login": 7001,
|
|
101
|
+
"password": "pw",
|
|
102
|
+
"nickname": "prop-1",
|
|
103
|
+
"trade_ea_symbol": "",
|
|
104
|
+
}
|
|
105
|
+
assert account.login == 7001
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@respx.mock
|
|
109
|
+
def test_slots_full_maps_to_typed_error() -> None:
|
|
110
|
+
respx.post(f"{BASE}/private-servers/{SERVER_ID}/accounts").mock(
|
|
111
|
+
return_value=httpx.Response(
|
|
112
|
+
409,
|
|
113
|
+
json={
|
|
114
|
+
"error": "slots_full",
|
|
115
|
+
"detail": "Server is full (2/2).",
|
|
116
|
+
"used": 2,
|
|
117
|
+
"cap": 2,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
with _client() as fx:
|
|
122
|
+
with pytest.raises(SlotsFullError) as err:
|
|
123
|
+
fx.private_servers.add_account(
|
|
124
|
+
SERVER_ID, server="Demo", login=1, password="pw"
|
|
125
|
+
)
|
|
126
|
+
assert err.value.used == 2
|
|
127
|
+
assert err.value.cap == 2
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@respx.mock
|
|
131
|
+
def test_duplicate_still_maps_to_duplicate_error() -> None:
|
|
132
|
+
respx.post(f"{BASE}/private-servers/{SERVER_ID}/accounts").mock(
|
|
133
|
+
return_value=httpx.Response(
|
|
134
|
+
409, json={"error": "duplicate", "detail": "Already linked."}
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
with _client() as fx:
|
|
138
|
+
with pytest.raises(DuplicateAccountError):
|
|
139
|
+
fx.private_servers.add_account(
|
|
140
|
+
SERVER_ID, server="Demo", login=1, password="pw"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@respx.mock
|
|
145
|
+
def test_remove_account() -> None:
|
|
146
|
+
account_id = SERVER_ACCOUNT["id"]
|
|
147
|
+
route = respx.delete(
|
|
148
|
+
f"{BASE}/private-servers/{SERVER_ID}/accounts/{account_id}"
|
|
149
|
+
).mock(return_value=httpx.Response(204))
|
|
150
|
+
with _client() as fx:
|
|
151
|
+
fx.private_servers.remove_account(SERVER_ID, account_id)
|
|
152
|
+
assert route.called
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@respx.mock
|
|
156
|
+
def test_terminal_client_from_private_account() -> None:
|
|
157
|
+
respx.get(f"{BASE}/private-servers").mock(
|
|
158
|
+
return_value=httpx.Response(200, json=[SERVER])
|
|
159
|
+
)
|
|
160
|
+
with _client() as fx:
|
|
161
|
+
[server] = fx.private_servers.list()
|
|
162
|
+
term = fx.terminal(server.accounts[0], verify=False)
|
|
163
|
+
assert term is not None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@pytest.mark.asyncio
|
|
167
|
+
@respx.mock
|
|
168
|
+
async def test_async_mirror() -> None:
|
|
169
|
+
respx.get(f"{BASE}/private-servers").mock(
|
|
170
|
+
return_value=httpx.Response(200, json=[SERVER])
|
|
171
|
+
)
|
|
172
|
+
respx.post(f"{BASE}/private-servers/{SERVER_ID}/accounts").mock(
|
|
173
|
+
return_value=httpx.Response(201, json=SERVER_ACCOUNT)
|
|
174
|
+
)
|
|
175
|
+
async with AsyncClient(api_key="fxs_live_test") as fx:
|
|
176
|
+
[server] = await fx.private_servers.list()
|
|
177
|
+
account = await fx.private_servers.add_account(
|
|
178
|
+
server, server="ICMarkets-Demo", login=7001, password="pw"
|
|
179
|
+
)
|
|
180
|
+
assert server.is_ready
|
|
181
|
+
assert account.login == 7001
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2"
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
"""Account management — the public v1 API (``/v1/accounts``).
|
|
2
|
-
|
|
3
|
-
Accessible as ``client.accounts`` on both :class:`~fxsocket.Client` and
|
|
4
|
-
:class:`~fxsocket.AsyncClient`.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from ._http import AsyncHTTP, SyncHTTP
|
|
10
|
-
from .enums import Platform
|
|
11
|
-
from .models import Account
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def account_id_of(account: Account | str) -> str:
|
|
15
|
-
"""Accept either an :class:`Account` or a bare id string."""
|
|
16
|
-
return account.id if isinstance(account, Account) else str(account)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _create_payload(
|
|
20
|
-
*,
|
|
21
|
-
server: str,
|
|
22
|
-
login: int,
|
|
23
|
-
password: str,
|
|
24
|
-
platform: Platform | str,
|
|
25
|
-
nickname: str,
|
|
26
|
-
) -> dict[str, object]:
|
|
27
|
-
return {
|
|
28
|
-
"platform": Platform(platform).value,
|
|
29
|
-
"server": server,
|
|
30
|
-
"login": login,
|
|
31
|
-
"password": password,
|
|
32
|
-
"nickname": nickname,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class Accounts:
|
|
37
|
-
"""Synchronous account operations."""
|
|
38
|
-
|
|
39
|
-
def __init__(self, http: SyncHTTP) -> None:
|
|
40
|
-
self._http = http
|
|
41
|
-
|
|
42
|
-
def list(self) -> list[Account]:
|
|
43
|
-
"""List every account owned by the authenticated user."""
|
|
44
|
-
data = self._http.request("GET", "/accounts")
|
|
45
|
-
return [Account.model_validate(row) for row in data]
|
|
46
|
-
|
|
47
|
-
def get(self, account: Account | str) -> Account:
|
|
48
|
-
"""Fetch one account by id (use this to poll connection status)."""
|
|
49
|
-
data = self._http.request("GET", f"/accounts/{account_id_of(account)}")
|
|
50
|
-
return Account.model_validate(data)
|
|
51
|
-
|
|
52
|
-
def create(
|
|
53
|
-
self,
|
|
54
|
-
*,
|
|
55
|
-
server: str,
|
|
56
|
-
login: int,
|
|
57
|
-
password: str,
|
|
58
|
-
platform: Platform | str = Platform.MT5,
|
|
59
|
-
nickname: str = "",
|
|
60
|
-
) -> Account:
|
|
61
|
-
"""Link (connect) a new MT4/MT5 account. Returns it in ``connecting``
|
|
62
|
-
state when terminal pods are enabled, else ``connected``."""
|
|
63
|
-
data = self._http.request(
|
|
64
|
-
"POST",
|
|
65
|
-
"/accounts",
|
|
66
|
-
json=_create_payload(
|
|
67
|
-
server=server,
|
|
68
|
-
login=login,
|
|
69
|
-
password=password,
|
|
70
|
-
platform=platform,
|
|
71
|
-
nickname=nickname,
|
|
72
|
-
),
|
|
73
|
-
)
|
|
74
|
-
return Account.model_validate(data)
|
|
75
|
-
|
|
76
|
-
def delete(self, account: Account | str) -> None:
|
|
77
|
-
"""Unlink (disconnect) an account and tear down its terminal."""
|
|
78
|
-
self._http.request("DELETE", f"/accounts/{account_id_of(account)}")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class AsyncAccounts:
|
|
82
|
-
"""Asynchronous mirror of :class:`Accounts`."""
|
|
83
|
-
|
|
84
|
-
def __init__(self, http: AsyncHTTP) -> None:
|
|
85
|
-
self._http = http
|
|
86
|
-
|
|
87
|
-
async def list(self) -> list[Account]:
|
|
88
|
-
data = await self._http.request("GET", "/accounts")
|
|
89
|
-
return [Account.model_validate(row) for row in data]
|
|
90
|
-
|
|
91
|
-
async def get(self, account: Account | str) -> Account:
|
|
92
|
-
data = await self._http.request("GET", f"/accounts/{account_id_of(account)}")
|
|
93
|
-
return Account.model_validate(data)
|
|
94
|
-
|
|
95
|
-
async def create(
|
|
96
|
-
self,
|
|
97
|
-
*,
|
|
98
|
-
server: str,
|
|
99
|
-
login: int,
|
|
100
|
-
password: str,
|
|
101
|
-
platform: Platform | str = Platform.MT5,
|
|
102
|
-
nickname: str = "",
|
|
103
|
-
) -> Account:
|
|
104
|
-
data = await self._http.request(
|
|
105
|
-
"POST",
|
|
106
|
-
"/accounts",
|
|
107
|
-
json=_create_payload(
|
|
108
|
-
server=server,
|
|
109
|
-
login=login,
|
|
110
|
-
password=password,
|
|
111
|
-
platform=platform,
|
|
112
|
-
nickname=nickname,
|
|
113
|
-
),
|
|
114
|
-
)
|
|
115
|
-
return Account.model_validate(data)
|
|
116
|
-
|
|
117
|
-
async def delete(self, account: Account | str) -> None:
|
|
118
|
-
await self._http.request("DELETE", f"/accounts/{account_id_of(account)}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|