fxsocket 0.1__py3-none-any.whl
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/__init__.py +134 -0
- fxsocket/_http.py +68 -0
- fxsocket/_version.py +1 -0
- fxsocket/client.py +254 -0
- fxsocket/config.py +12 -0
- fxsocket/enums.py +122 -0
- fxsocket/errors.py +163 -0
- fxsocket/management.py +118 -0
- fxsocket/models.py +391 -0
- fxsocket/py.typed +0 -0
- fxsocket/terminal/__init__.py +45 -0
- fxsocket/terminal/client.py +757 -0
- fxsocket/terminal/stream.py +594 -0
- fxsocket-0.1.dist-info/METADATA +256 -0
- fxsocket-0.1.dist-info/RECORD +17 -0
- fxsocket-0.1.dist-info/WHEEL +4 -0
- fxsocket-0.1.dist-info/licenses/LICENSE +21 -0
fxsocket/errors.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Exception hierarchy and HTTP-response → exception mapping.
|
|
2
|
+
|
|
3
|
+
A single :func:`error_from_response` maps both error envelopes the platform
|
|
4
|
+
uses — the management API's ``{"error", "detail"}`` and the terminal API's
|
|
5
|
+
``{"error", "message", "command_id"}`` — onto typed exceptions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FxSocketError(Exception):
|
|
16
|
+
"""Base class for every error raised by the SDK."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
message: str,
|
|
21
|
+
*,
|
|
22
|
+
status_code: int | None = None,
|
|
23
|
+
code: str | None = None,
|
|
24
|
+
response: httpx.Response | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.message = message
|
|
28
|
+
self.status_code = status_code
|
|
29
|
+
self.code = code
|
|
30
|
+
self.response = response
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AuthError(FxSocketError):
|
|
34
|
+
"""Missing or invalid API key (HTTP 401), or no key configured."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RateLimitError(FxSocketError):
|
|
38
|
+
"""Too many requests (HTTP 429). ``retry_after`` is seconds, if given."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, message: str, *, retry_after: float | None = None, **kw: Any):
|
|
41
|
+
super().__init__(message, **kw)
|
|
42
|
+
self.retry_after = retry_after
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ValidationError(FxSocketError):
|
|
46
|
+
"""The request was rejected as malformed (HTTP 400 / ``MRPC_VALIDATION``)."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class NotFoundError(FxSocketError):
|
|
50
|
+
"""The referenced account or resource does not exist (HTTP 404)."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AccountCapError(FxSocketError):
|
|
54
|
+
"""Plan account limit reached (HTTP 402 ``account_cap_reached``)."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
message: str,
|
|
59
|
+
*,
|
|
60
|
+
cap: int | None = None,
|
|
61
|
+
current: int | None = None,
|
|
62
|
+
**kw: Any,
|
|
63
|
+
):
|
|
64
|
+
super().__init__(message, **kw)
|
|
65
|
+
self.cap = cap
|
|
66
|
+
self.current = current
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class NoSubscriptionError(FxSocketError):
|
|
70
|
+
"""No plan permits linking accounts (HTTP 402 ``no_subscription``)."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DuplicateAccountError(FxSocketError):
|
|
74
|
+
"""This account is already linked (HTTP 409)."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ConnectFailedError(FxSocketError):
|
|
78
|
+
"""The broker rejected the login during account creation (HTTP 400).
|
|
79
|
+
|
|
80
|
+
``code`` is one of ``invalid_credentials``, ``server_not_found``,
|
|
81
|
+
``unknown``.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TerminalNotReadyError(FxSocketError):
|
|
86
|
+
"""The account's terminal isn't reachable yet.
|
|
87
|
+
|
|
88
|
+
Raised on HTTP 503 (trade EA not registered) and when an account has no
|
|
89
|
+
``rest_url`` — it is still provisioning, or is bridge-only and exposes no
|
|
90
|
+
per-account terminal API.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TerminalTimeoutError(FxSocketError):
|
|
95
|
+
"""The terminal didn't answer in time (HTTP 504 / ``MRPC_TIMEOUT``)."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class UnsupportedOnPlatformError(FxSocketError):
|
|
99
|
+
"""A requested feature doesn't exist on the account's platform.
|
|
100
|
+
|
|
101
|
+
Enforced client-side — e.g. stop-limit orders or MT5-only timeframes on
|
|
102
|
+
an MT4 account.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class StreamError(FxSocketError):
|
|
107
|
+
"""A WebSocket-level error (server error frame, or dropped connection)."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_CONNECT_CODES = frozenset({"invalid_credentials", "server_not_found", "unknown"})
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def error_from_response(resp: httpx.Response) -> FxSocketError:
|
|
114
|
+
"""Build the most specific :class:`FxSocketError` for a failed response."""
|
|
115
|
+
status = resp.status_code
|
|
116
|
+
body: Any = None
|
|
117
|
+
try:
|
|
118
|
+
body = resp.json()
|
|
119
|
+
except ValueError:
|
|
120
|
+
body = None
|
|
121
|
+
|
|
122
|
+
code: str | None = None
|
|
123
|
+
detail: str | None = None
|
|
124
|
+
if isinstance(body, dict):
|
|
125
|
+
code = body.get("error")
|
|
126
|
+
detail = body.get("detail") or body.get("message")
|
|
127
|
+
message = detail or code or f"HTTP {status}"
|
|
128
|
+
common: dict[str, Any] = {
|
|
129
|
+
"status_code": status,
|
|
130
|
+
"code": code,
|
|
131
|
+
"response": resp,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if status == 401:
|
|
135
|
+
return AuthError(message, **common)
|
|
136
|
+
if status == 429:
|
|
137
|
+
raw = resp.headers.get("Retry-After")
|
|
138
|
+
retry = None
|
|
139
|
+
if raw:
|
|
140
|
+
try:
|
|
141
|
+
retry = float(raw)
|
|
142
|
+
except ValueError:
|
|
143
|
+
retry = None
|
|
144
|
+
return RateLimitError(message, retry_after=retry, **common)
|
|
145
|
+
if status == 404:
|
|
146
|
+
return NotFoundError(message, **common)
|
|
147
|
+
if status == 409:
|
|
148
|
+
return DuplicateAccountError(message, **common)
|
|
149
|
+
if status == 402:
|
|
150
|
+
if code == "account_cap_reached" and isinstance(body, dict):
|
|
151
|
+
return AccountCapError(
|
|
152
|
+
message, cap=body.get("cap"), current=body.get("current"), **common
|
|
153
|
+
)
|
|
154
|
+
return NoSubscriptionError(message, **common)
|
|
155
|
+
if status == 400:
|
|
156
|
+
if code in _CONNECT_CODES:
|
|
157
|
+
return ConnectFailedError(message, **common)
|
|
158
|
+
return ValidationError(message, **common)
|
|
159
|
+
if status == 503:
|
|
160
|
+
return TerminalNotReadyError(message, **common)
|
|
161
|
+
if status == 504:
|
|
162
|
+
return TerminalTimeoutError(message, **common)
|
|
163
|
+
return FxSocketError(message, **common)
|
fxsocket/management.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
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)}")
|
fxsocket/models.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Pydantic models for FxSocket API payloads.
|
|
2
|
+
|
|
3
|
+
Two families:
|
|
4
|
+
|
|
5
|
+
* The management model (:class:`Account`) — the v1 API, which already speaks
|
|
6
|
+
``snake_case`` and returns a genuine UTC ``created_at``.
|
|
7
|
+
* Terminal payloads — ``camelCase`` on the wire (accepted via aliases). Two
|
|
8
|
+
deliberate typing choices keep these robust:
|
|
9
|
+
|
|
10
|
+
- **MetaTrader vocabulary fields** (``type``, ``kind``, ``entry``,
|
|
11
|
+
``status``) are plain ``str``, not enums: the exact serialized casing
|
|
12
|
+
varies and a strict enum would raise on an unrecognized value. Compare
|
|
13
|
+
them against the str-enums in :mod:`fxsocket.enums` (``OrderOperation``,
|
|
14
|
+
``HealthStatus``, …) — those compare equal to the raw string.
|
|
15
|
+
- **Timestamps** are ``str``, not ``datetime``: they are in *broker server
|
|
16
|
+
time* with a stylistic trailing ``Z``, so decoding them as UTC would be
|
|
17
|
+
silently wrong. Use :class:`ServerTimezone` to convert if needed.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, ConfigDict
|
|
25
|
+
from pydantic.alias_generators import to_camel
|
|
26
|
+
|
|
27
|
+
from .enums import Platform, TradingStatus
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _Camel(BaseModel):
|
|
31
|
+
"""Terminal payloads: read camelCase aliases, also accept field names."""
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(
|
|
34
|
+
populate_by_name=True,
|
|
35
|
+
alias_generator=to_camel,
|
|
36
|
+
extra="ignore",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# --------------------------------------------------------------------------- #
|
|
41
|
+
# Management API (v1)
|
|
42
|
+
# --------------------------------------------------------------------------- #
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Account(BaseModel):
|
|
46
|
+
"""A linked trading account, as returned by the management API (v1).
|
|
47
|
+
|
|
48
|
+
``rest_url`` / ``ws_url`` are where this account's terminal REST and
|
|
49
|
+
WebSocket APIs live. Both are empty until the account has a reachable
|
|
50
|
+
terminal (shared pod or private droplet); a bridge-only account exposes
|
|
51
|
+
none.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
|
55
|
+
|
|
56
|
+
id: str
|
|
57
|
+
nickname: str = ""
|
|
58
|
+
platform: Platform
|
|
59
|
+
server: str
|
|
60
|
+
login: int
|
|
61
|
+
status: TradingStatus
|
|
62
|
+
error: str = ""
|
|
63
|
+
rest_url: str = ""
|
|
64
|
+
ws_url: str = ""
|
|
65
|
+
created_at: datetime
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def has_terminal(self) -> bool:
|
|
69
|
+
"""True when this account exposes a reachable terminal API."""
|
|
70
|
+
return bool(self.rest_url)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --------------------------------------------------------------------------- #
|
|
74
|
+
# Terminal — account state
|
|
75
|
+
# --------------------------------------------------------------------------- #
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AccountSummary(_Camel):
|
|
79
|
+
"""Live financial snapshot (``GET /AccountSummary``)."""
|
|
80
|
+
|
|
81
|
+
balance: float
|
|
82
|
+
credit: float
|
|
83
|
+
profit: float
|
|
84
|
+
equity: float
|
|
85
|
+
margin: float
|
|
86
|
+
free_margin: float
|
|
87
|
+
margin_level: float
|
|
88
|
+
leverage: int
|
|
89
|
+
currency: str
|
|
90
|
+
type: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AccountInfo(_Camel):
|
|
94
|
+
"""Static account identity + configuration (``GET /AccountInfo``).
|
|
95
|
+
|
|
96
|
+
On MT4 ``margin_mode`` is always ``"Hedging"`` and ``fifo_close`` always
|
|
97
|
+
``False`` (the platform has no native equivalent).
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
name: str
|
|
101
|
+
login: int
|
|
102
|
+
server: str
|
|
103
|
+
company: str
|
|
104
|
+
currency: str
|
|
105
|
+
currency_digits: int
|
|
106
|
+
leverage: int
|
|
107
|
+
type: str
|
|
108
|
+
margin_mode: str
|
|
109
|
+
margin_so_mode: str
|
|
110
|
+
margin_call_level: float
|
|
111
|
+
stop_out_level: float
|
|
112
|
+
trade_allowed: bool
|
|
113
|
+
trade_expert: bool
|
|
114
|
+
limit_orders: int
|
|
115
|
+
fifo_close: bool
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class OpenedOrder(_Camel):
|
|
119
|
+
"""An open position or resting pending order (``GET /OpenedOrders``)."""
|
|
120
|
+
|
|
121
|
+
ticket: int
|
|
122
|
+
symbol: str
|
|
123
|
+
type: str
|
|
124
|
+
kind: str
|
|
125
|
+
lots: float
|
|
126
|
+
open_price: float
|
|
127
|
+
current_price: float
|
|
128
|
+
stop_loss: float
|
|
129
|
+
take_profit: float
|
|
130
|
+
swap: float
|
|
131
|
+
profit: float
|
|
132
|
+
magic: int
|
|
133
|
+
comment: str
|
|
134
|
+
open_time: str
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def is_pending(self) -> bool:
|
|
138
|
+
"""True for a resting pending order (vs. a live position)."""
|
|
139
|
+
return self.kind.lower() == "pending"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class HistoryTrade(_Camel):
|
|
143
|
+
"""A historical deal / closed order (``GET /OrderHistory``).
|
|
144
|
+
|
|
145
|
+
On MT4 this is one row per closed *order* (no per-deal granularity);
|
|
146
|
+
``order`` aliases the ticket and ``entry`` is constant.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
ticket: int
|
|
150
|
+
order: int
|
|
151
|
+
symbol: str
|
|
152
|
+
type: str
|
|
153
|
+
entry: str
|
|
154
|
+
volume: float
|
|
155
|
+
price: float
|
|
156
|
+
commission: float
|
|
157
|
+
swap: float
|
|
158
|
+
profit: float
|
|
159
|
+
magic: int
|
|
160
|
+
comment: str
|
|
161
|
+
time: str
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class PositionTrade(_Camel):
|
|
165
|
+
"""A closed round-trip position (``GET /PositionHistory``)."""
|
|
166
|
+
|
|
167
|
+
position_id: int
|
|
168
|
+
symbol: str
|
|
169
|
+
type: str
|
|
170
|
+
volume: float
|
|
171
|
+
open_time: str
|
|
172
|
+
open_price: float
|
|
173
|
+
close_time: str
|
|
174
|
+
close_price: float
|
|
175
|
+
profit: float
|
|
176
|
+
swap: float
|
|
177
|
+
commission: float
|
|
178
|
+
net_profit: float
|
|
179
|
+
magic: int
|
|
180
|
+
comment: str
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class ServerTimezone(_Camel):
|
|
184
|
+
"""Broker server clock + UTC offset (``GET /ServerTimezone``).
|
|
185
|
+
|
|
186
|
+
``utc_offset_seconds`` is ``server_time - UTC``; subtract it from a
|
|
187
|
+
broker-server timestamp to get UTC.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
server_time: str
|
|
191
|
+
utc_offset_seconds: int
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# --------------------------------------------------------------------------- #
|
|
195
|
+
# Terminal — market data
|
|
196
|
+
# --------------------------------------------------------------------------- #
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Quote(_Camel):
|
|
200
|
+
"""Latest tick for a symbol (``GET /getQuote``).
|
|
201
|
+
|
|
202
|
+
``last`` / ``volume`` are ~0 on forex (and always 0 on MT4).
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
symbol: str
|
|
206
|
+
bid: float
|
|
207
|
+
ask: float
|
|
208
|
+
time: str
|
|
209
|
+
last: float
|
|
210
|
+
volume: int
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class SymbolInfo(_Camel):
|
|
214
|
+
"""Contract specification for a symbol (``GET /SymbolInfo``)."""
|
|
215
|
+
|
|
216
|
+
symbol: str
|
|
217
|
+
description: str
|
|
218
|
+
digits: int
|
|
219
|
+
point: float
|
|
220
|
+
tick_size: float
|
|
221
|
+
tick_value: float
|
|
222
|
+
contract_size: float
|
|
223
|
+
volume_min: float
|
|
224
|
+
volume_max: float
|
|
225
|
+
volume_step: float
|
|
226
|
+
stops_level: int
|
|
227
|
+
freeze_level: int
|
|
228
|
+
spread: int
|
|
229
|
+
trade_mode: str
|
|
230
|
+
swap_long: float
|
|
231
|
+
swap_short: float
|
|
232
|
+
bid: float
|
|
233
|
+
ask: float
|
|
234
|
+
currency_base: str
|
|
235
|
+
currency_profit: str
|
|
236
|
+
currency_margin: str
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class Candle(_Camel):
|
|
240
|
+
"""One OHLC bar (``GET /PriceHistory``). ``real_volume`` is 0 on MT4."""
|
|
241
|
+
|
|
242
|
+
time: str
|
|
243
|
+
open: float
|
|
244
|
+
high: float
|
|
245
|
+
low: float
|
|
246
|
+
close: float
|
|
247
|
+
tick_volume: int
|
|
248
|
+
real_volume: int
|
|
249
|
+
spread: int
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# --------------------------------------------------------------------------- #
|
|
253
|
+
# Terminal — trading
|
|
254
|
+
# --------------------------------------------------------------------------- #
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class OrderResult(_Camel):
|
|
258
|
+
"""Result of an order send / modify / close.
|
|
259
|
+
|
|
260
|
+
``success`` is true when ``retcode`` is DONE (10009) or PLACED (10010).
|
|
261
|
+
``deal`` is the executed deal ticket (0 for pending placement, and always
|
|
262
|
+
0 on MT4); ``order`` is the resulting position / pending-order ticket.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
success: bool
|
|
266
|
+
retcode: int
|
|
267
|
+
retcode_description: str
|
|
268
|
+
deal: int
|
|
269
|
+
order: int
|
|
270
|
+
volume: float
|
|
271
|
+
price: float
|
|
272
|
+
bid: float
|
|
273
|
+
ask: float
|
|
274
|
+
comment: str
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class MarginCalc(_Camel):
|
|
278
|
+
"""Required margin for a hypothetical order (``GET /OrderCalcMargin``)."""
|
|
279
|
+
|
|
280
|
+
symbol: str
|
|
281
|
+
operation: str
|
|
282
|
+
volume: float
|
|
283
|
+
price: float
|
|
284
|
+
margin: float
|
|
285
|
+
currency: str
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class ProfitCalc(_Camel):
|
|
289
|
+
"""Projected P/L for a hypothetical trade (``GET /OrderCalcProfit``)."""
|
|
290
|
+
|
|
291
|
+
symbol: str
|
|
292
|
+
operation: str
|
|
293
|
+
volume: float
|
|
294
|
+
price_open: float
|
|
295
|
+
price_close: float
|
|
296
|
+
profit: float
|
|
297
|
+
currency: str
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# --------------------------------------------------------------------------- #
|
|
301
|
+
# Terminal — health
|
|
302
|
+
# --------------------------------------------------------------------------- #
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class TerminalHealth(_Camel):
|
|
306
|
+
alive: bool
|
|
307
|
+
build: int = 0
|
|
308
|
+
ping_ms: int = 0
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class BrokerHealth(_Camel):
|
|
312
|
+
connected: bool
|
|
313
|
+
server: str = ""
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class AccountHealth(_Camel):
|
|
317
|
+
"""Account section of ``/status``. ``currency`` / ``type`` are blank when
|
|
318
|
+
not logged in; ``login`` is always the configured account."""
|
|
319
|
+
|
|
320
|
+
logged_in: bool
|
|
321
|
+
login: int = 0
|
|
322
|
+
currency: str = ""
|
|
323
|
+
type: str = ""
|
|
324
|
+
trade_allowed: bool = False
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class BridgeHealth(_Camel):
|
|
328
|
+
version: str = ""
|
|
329
|
+
trade_ea_ready: bool = False
|
|
330
|
+
symbols_synced: bool = False
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class Health(_Camel):
|
|
334
|
+
"""Full health snapshot (``GET /status``) — always HTTP 200.
|
|
335
|
+
|
|
336
|
+
``status`` is one of ``ready`` / ``starting`` / ``degraded`` / ``down``
|
|
337
|
+
(compare against :class:`fxsocket.HealthStatus`).
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
status: str
|
|
341
|
+
terminal: TerminalHealth
|
|
342
|
+
broker: BrokerHealth
|
|
343
|
+
account: AccountHealth
|
|
344
|
+
bridge: BridgeHealth
|
|
345
|
+
server_time: str = ""
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def is_ready(self) -> bool:
|
|
349
|
+
return self.status == "ready"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class HealthChecks(_Camel):
|
|
353
|
+
"""PII-free probe body from ``/healthz`` and ``/livez``."""
|
|
354
|
+
|
|
355
|
+
status: str
|
|
356
|
+
terminal: bool
|
|
357
|
+
broker: bool
|
|
358
|
+
account: bool
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# --------------------------------------------------------------------------- #
|
|
362
|
+
# Terminal — streaming payloads (the inner ``data`` of some WS events)
|
|
363
|
+
# --------------------------------------------------------------------------- #
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class TradeEventData(_Camel):
|
|
367
|
+
"""A trade transaction pushed on the ``trades`` stream.
|
|
368
|
+
|
|
369
|
+
``deal`` / ``position`` are 0 on MT4 (no per-deal model); ``entry`` is the
|
|
370
|
+
deal direction (compare against :class:`fxsocket.DealEntry`).
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
deal: int
|
|
374
|
+
order: int
|
|
375
|
+
position: int
|
|
376
|
+
symbol: str
|
|
377
|
+
type: str
|
|
378
|
+
entry: str
|
|
379
|
+
volume: float
|
|
380
|
+
price: float
|
|
381
|
+
profit: float
|
|
382
|
+
comment: str
|
|
383
|
+
time: str
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class TerminalStatusData(_Camel):
|
|
387
|
+
"""Terminal status pushed (~1/s) on the ``terminal`` stream."""
|
|
388
|
+
|
|
389
|
+
connected: bool
|
|
390
|
+
trade_allowed: bool
|
|
391
|
+
server_time: str
|
fxsocket/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Per-account terminal API — REST trading/market-data and WebSocket streaming.
|
|
2
|
+
|
|
3
|
+
The endpoints for an account come from ``Account.rest_url`` / ``Account.ws_url``
|
|
4
|
+
(populated by the management API), which already resolve shared-pod vs
|
|
5
|
+
private-droplet hosting.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .client import AsyncTerminalClient, TerminalClient
|
|
9
|
+
from .stream import (
|
|
10
|
+
AccountUpdate,
|
|
11
|
+
AsyncStream,
|
|
12
|
+
Bar,
|
|
13
|
+
PositionsUpdate,
|
|
14
|
+
Stream,
|
|
15
|
+
StreamErrorEvent,
|
|
16
|
+
StreamEvent,
|
|
17
|
+
StreamWarning,
|
|
18
|
+
Subscribed,
|
|
19
|
+
Subscriptions,
|
|
20
|
+
TerminalUpdate,
|
|
21
|
+
Tick,
|
|
22
|
+
TradeUpdate,
|
|
23
|
+
UnknownEvent,
|
|
24
|
+
Unsubscribed,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"TerminalClient",
|
|
29
|
+
"AsyncTerminalClient",
|
|
30
|
+
"AsyncStream",
|
|
31
|
+
"Stream",
|
|
32
|
+
"StreamEvent",
|
|
33
|
+
"Tick",
|
|
34
|
+
"Bar",
|
|
35
|
+
"AccountUpdate",
|
|
36
|
+
"PositionsUpdate",
|
|
37
|
+
"TradeUpdate",
|
|
38
|
+
"TerminalUpdate",
|
|
39
|
+
"StreamWarning",
|
|
40
|
+
"Subscribed",
|
|
41
|
+
"Unsubscribed",
|
|
42
|
+
"StreamErrorEvent",
|
|
43
|
+
"Subscriptions",
|
|
44
|
+
"UnknownEvent",
|
|
45
|
+
]
|