fundedops 0.1.0__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.
- fundedops/__init__.py +105 -0
- fundedops/_async_client.py +222 -0
- fundedops/_base.py +230 -0
- fundedops/_client.py +237 -0
- fundedops/_streaming.py +108 -0
- fundedops/errors.py +157 -0
- fundedops/models.py +266 -0
- fundedops/py.typed +0 -0
- fundedops-0.1.0.dist-info/METADATA +127 -0
- fundedops-0.1.0.dist-info/RECORD +11 -0
- fundedops-0.1.0.dist-info/WHEEL +4 -0
fundedops/__init__.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""fundedops — the official Python SDK for the FundedOps API.
|
|
2
|
+
|
|
3
|
+
This package ships the contract layer the HTTP and streaming clients build on:
|
|
4
|
+
|
|
5
|
+
* :mod:`fundedops.models` — pydantic v2 models that mirror the camelCase wire
|
|
6
|
+
format exactly (snake_case attributes, camelCase aliases).
|
|
7
|
+
* :mod:`fundedops.errors` — a typed exception hierarchy mirroring the API's
|
|
8
|
+
MetaApi-style error envelope.
|
|
9
|
+
* :class:`FundedOps` / :class:`AsyncFundedOps` — the synchronous and
|
|
10
|
+
asynchronous REST + streaming clients (first-class equals, D-17).
|
|
11
|
+
* :class:`Stream` / :class:`AsyncStream` — the WS event iterators returned by
|
|
12
|
+
``client.stream(...)``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from ._async_client import AsyncFundedOps
|
|
16
|
+
from ._base import DEFAULT_BASE_URL
|
|
17
|
+
from ._client import FundedOps
|
|
18
|
+
from ._streaming import AsyncStream, Stream, StreamClosed
|
|
19
|
+
from .errors import (
|
|
20
|
+
ConflictError,
|
|
21
|
+
ForbiddenError,
|
|
22
|
+
FundedOpsError,
|
|
23
|
+
InternalError,
|
|
24
|
+
NotFoundError,
|
|
25
|
+
PaymentRequiredError,
|
|
26
|
+
RateLimitedError,
|
|
27
|
+
UnauthorizedError,
|
|
28
|
+
ValidationError,
|
|
29
|
+
raise_for_envelope,
|
|
30
|
+
)
|
|
31
|
+
from .models import (
|
|
32
|
+
Account,
|
|
33
|
+
AccountInformation,
|
|
34
|
+
AccountInformationEvent,
|
|
35
|
+
AccountInformationUpdatedEvent,
|
|
36
|
+
Deal,
|
|
37
|
+
DealAddedEvent,
|
|
38
|
+
DealsPage,
|
|
39
|
+
ErrorEvent,
|
|
40
|
+
Order,
|
|
41
|
+
OrdersEvent,
|
|
42
|
+
OrdersPage,
|
|
43
|
+
OrdersUpdatedEvent,
|
|
44
|
+
Page,
|
|
45
|
+
PingEvent,
|
|
46
|
+
Position,
|
|
47
|
+
PositionsEvent,
|
|
48
|
+
PositionsPage,
|
|
49
|
+
PositionsUpdatedEvent,
|
|
50
|
+
StatusEvent,
|
|
51
|
+
SyncFinishedEvent,
|
|
52
|
+
SyncStartedEvent,
|
|
53
|
+
WSEvent,
|
|
54
|
+
parse_event,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
__version__ = "0.1.0"
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"__version__",
|
|
61
|
+
"DEFAULT_BASE_URL",
|
|
62
|
+
# Clients
|
|
63
|
+
"FundedOps",
|
|
64
|
+
"AsyncFundedOps",
|
|
65
|
+
# Streaming
|
|
66
|
+
"Stream",
|
|
67
|
+
"AsyncStream",
|
|
68
|
+
"StreamClosed",
|
|
69
|
+
# Resources
|
|
70
|
+
"Account",
|
|
71
|
+
"AccountInformation",
|
|
72
|
+
"Position",
|
|
73
|
+
"Order",
|
|
74
|
+
"Deal",
|
|
75
|
+
"Page",
|
|
76
|
+
"PositionsPage",
|
|
77
|
+
"OrdersPage",
|
|
78
|
+
"DealsPage",
|
|
79
|
+
# WebSocket events
|
|
80
|
+
"parse_event",
|
|
81
|
+
"WSEvent",
|
|
82
|
+
"SyncStartedEvent",
|
|
83
|
+
"SyncFinishedEvent",
|
|
84
|
+
"AccountInformationEvent",
|
|
85
|
+
"AccountInformationUpdatedEvent",
|
|
86
|
+
"PositionsEvent",
|
|
87
|
+
"PositionsUpdatedEvent",
|
|
88
|
+
"OrdersEvent",
|
|
89
|
+
"OrdersUpdatedEvent",
|
|
90
|
+
"DealAddedEvent",
|
|
91
|
+
"StatusEvent",
|
|
92
|
+
"PingEvent",
|
|
93
|
+
"ErrorEvent",
|
|
94
|
+
# Errors
|
|
95
|
+
"raise_for_envelope",
|
|
96
|
+
"FundedOpsError",
|
|
97
|
+
"ValidationError",
|
|
98
|
+
"UnauthorizedError",
|
|
99
|
+
"PaymentRequiredError",
|
|
100
|
+
"ForbiddenError",
|
|
101
|
+
"NotFoundError",
|
|
102
|
+
"ConflictError",
|
|
103
|
+
"RateLimitedError",
|
|
104
|
+
"InternalError",
|
|
105
|
+
]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Asynchronous FundedOps client over ``httpx.AsyncClient``.
|
|
2
|
+
|
|
3
|
+
A method-for-method mirror of :mod:`fundedops._client` — same names, same
|
|
4
|
+
signatures, awaitable. D-17 makes the async client a first-class equal, so any
|
|
5
|
+
method present in one and absent from the other is a defect; ``test_async_client``
|
|
6
|
+
introspects both classes and asserts the public method sets are identical. All
|
|
7
|
+
request/param/body/parse logic is shared through :mod:`fundedops._base`, so this
|
|
8
|
+
module only adds ``await`` around the network boundary.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from . import _base
|
|
20
|
+
from ._base import UNSET, DEFAULT_BASE_URL, DEFAULT_TIMEOUT
|
|
21
|
+
from .errors import raise_for_envelope
|
|
22
|
+
from .models import Account, AccountInformation, DealsPage, OrdersPage, PositionsPage
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _AsyncAccounts:
|
|
26
|
+
"""The async ``client.accounts`` namespace — mirrors ``_client._Accounts``."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, client: "AsyncFundedOps") -> None:
|
|
29
|
+
self._client = client
|
|
30
|
+
|
|
31
|
+
# --- lifecycle ---------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
async def list(
|
|
34
|
+
self,
|
|
35
|
+
offset: int = 0,
|
|
36
|
+
limit: int | None = None,
|
|
37
|
+
state: str | None = None,
|
|
38
|
+
tags: str | None = None,
|
|
39
|
+
) -> list[Account]:
|
|
40
|
+
data = await self._client._request(
|
|
41
|
+
_base.list_accounts_spec(offset, limit, state, tags)
|
|
42
|
+
)
|
|
43
|
+
return _base.parse_accounts(data)
|
|
44
|
+
|
|
45
|
+
async def create(
|
|
46
|
+
self,
|
|
47
|
+
login: str,
|
|
48
|
+
password: str,
|
|
49
|
+
server: str,
|
|
50
|
+
platform: str,
|
|
51
|
+
name: str | None = None,
|
|
52
|
+
tags: list[str] | None = None,
|
|
53
|
+
metadata: dict | None = None,
|
|
54
|
+
) -> Account:
|
|
55
|
+
data = await self._client._request(
|
|
56
|
+
_base.create_account_spec(
|
|
57
|
+
login, password, server, platform, name, tags, metadata
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
return _base.parse_account(data)
|
|
61
|
+
|
|
62
|
+
async def get(self, account_id: str) -> Account:
|
|
63
|
+
return _base.parse_account(
|
|
64
|
+
await self._client._request(_base.get_account_spec(account_id))
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def update(
|
|
68
|
+
self,
|
|
69
|
+
account_id: str,
|
|
70
|
+
name: Any = UNSET,
|
|
71
|
+
tags: Any = UNSET,
|
|
72
|
+
metadata: Any = UNSET,
|
|
73
|
+
) -> Account:
|
|
74
|
+
return _base.parse_account(
|
|
75
|
+
await self._client._request(
|
|
76
|
+
_base.update_account_spec(account_id, name, tags, metadata)
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def delete(self, account_id: str) -> Account:
|
|
81
|
+
return _base.parse_account(
|
|
82
|
+
await self._client._request(_base.delete_account_spec(account_id))
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async def deploy(self, account_id: str) -> Account:
|
|
86
|
+
return _base.parse_account(
|
|
87
|
+
await self._client._request(_base.deploy_spec(account_id))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def undeploy(self, account_id: str) -> Account:
|
|
91
|
+
return _base.parse_account(
|
|
92
|
+
await self._client._request(_base.undeploy_spec(account_id))
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def redeploy(self, account_id: str) -> Account:
|
|
96
|
+
return _base.parse_account(
|
|
97
|
+
await self._client._request(_base.redeploy_spec(account_id))
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# --- reads -------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
async def account_information(self, account_id: str) -> AccountInformation:
|
|
103
|
+
return _base.parse_account_information(
|
|
104
|
+
await self._client._request(_base.account_information_spec(account_id))
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def positions(self, account_id: str) -> PositionsPage:
|
|
108
|
+
return _base.parse_positions_page(
|
|
109
|
+
await self._client._request(_base.positions_spec(account_id))
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def pending_orders(self, account_id: str) -> OrdersPage:
|
|
113
|
+
return _base.parse_orders_page(
|
|
114
|
+
await self._client._request(_base.pending_orders_spec(account_id))
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def history_deals(
|
|
118
|
+
self,
|
|
119
|
+
account_id: str,
|
|
120
|
+
start_time: str | None = None,
|
|
121
|
+
end_time: str | None = None,
|
|
122
|
+
ticket: str | None = None,
|
|
123
|
+
position: str | None = None,
|
|
124
|
+
offset: int = 0,
|
|
125
|
+
limit: int | None = None,
|
|
126
|
+
) -> DealsPage:
|
|
127
|
+
return _base.parse_deals_page(
|
|
128
|
+
await self._client._request(
|
|
129
|
+
_base.history_deals_spec(
|
|
130
|
+
account_id, start_time, end_time, ticket, position, offset, limit
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def history_orders(
|
|
136
|
+
self,
|
|
137
|
+
account_id: str,
|
|
138
|
+
start_time: str | None = None,
|
|
139
|
+
end_time: str | None = None,
|
|
140
|
+
ticket: str | None = None,
|
|
141
|
+
position: str | None = None,
|
|
142
|
+
offset: int = 0,
|
|
143
|
+
limit: int | None = None,
|
|
144
|
+
) -> OrdersPage:
|
|
145
|
+
return _base.parse_orders_page(
|
|
146
|
+
await self._client._request(
|
|
147
|
+
_base.history_orders_spec(
|
|
148
|
+
account_id, start_time, end_time, ticket, position, offset, limit
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class AsyncFundedOps:
|
|
155
|
+
"""Asynchronous FundedOps API client — the awaitable mirror of
|
|
156
|
+
:class:`fundedops.FundedOps`. Same constructor, same method surface."""
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
api_key: str,
|
|
161
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
162
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
163
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
164
|
+
) -> None:
|
|
165
|
+
self._api_key = api_key
|
|
166
|
+
self._base_url = base_url
|
|
167
|
+
self._http = httpx.AsyncClient(
|
|
168
|
+
base_url=base_url,
|
|
169
|
+
headers=_base.build_headers(api_key),
|
|
170
|
+
timeout=timeout,
|
|
171
|
+
transport=transport,
|
|
172
|
+
)
|
|
173
|
+
self.accounts = _AsyncAccounts(self)
|
|
174
|
+
|
|
175
|
+
async def _request(self, spec: _base.RequestSpec) -> Any:
|
|
176
|
+
"""Await one RequestSpec, translate any error envelope, return the decoded
|
|
177
|
+
JSON body — the single point every response flows through
|
|
178
|
+
``raise_for_envelope`` (mirrors the sync client)."""
|
|
179
|
+
response = await self._http.request(
|
|
180
|
+
spec.method, spec.path, params=spec.params, json=spec.json
|
|
181
|
+
)
|
|
182
|
+
raise_for_envelope(response)
|
|
183
|
+
return response.json()
|
|
184
|
+
|
|
185
|
+
async def wait_until_connected(
|
|
186
|
+
self, account_id: str, timeout: float, interval: float = 2.0
|
|
187
|
+
) -> Account:
|
|
188
|
+
"""Await polling of ``GET /v1/accounts/{id}`` until connected (D-16).
|
|
189
|
+
|
|
190
|
+
Raises :class:`TimeoutError` past ``timeout`` (T-19-16 — mandatory bound).
|
|
191
|
+
``interval=0`` keeps tests free of real awaits between polls."""
|
|
192
|
+
deadline = time.monotonic() + timeout
|
|
193
|
+
while True:
|
|
194
|
+
account = await self.accounts.get(account_id)
|
|
195
|
+
if account.connection_status == "connected":
|
|
196
|
+
return account
|
|
197
|
+
if time.monotonic() >= deadline:
|
|
198
|
+
raise TimeoutError(
|
|
199
|
+
f"account {account_id} did not connect within {timeout}s "
|
|
200
|
+
f"(last connectionStatus={account.connection_status!r})"
|
|
201
|
+
)
|
|
202
|
+
if interval:
|
|
203
|
+
await asyncio.sleep(interval)
|
|
204
|
+
|
|
205
|
+
def stream(self, account_ids: list[str]):
|
|
206
|
+
"""Open a WS stream and async-iterate typed events for ``account_ids``.
|
|
207
|
+
|
|
208
|
+
Returns an async-iterable :class:`fundedops._streaming.AsyncStream`
|
|
209
|
+
(imported lazily so the REST client carries no import-time websockets
|
|
210
|
+
dependency)."""
|
|
211
|
+
from ._streaming import AsyncStream
|
|
212
|
+
|
|
213
|
+
return AsyncStream(self._base_url, self._api_key, account_ids)
|
|
214
|
+
|
|
215
|
+
async def aclose(self) -> None:
|
|
216
|
+
await self._http.aclose()
|
|
217
|
+
|
|
218
|
+
async def __aenter__(self) -> "AsyncFundedOps":
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
222
|
+
await self.aclose()
|
fundedops/_base.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Shared SDK internals — the single source of truth both clients build on.
|
|
2
|
+
|
|
3
|
+
The synchronous (:mod:`fundedops._client`) and asynchronous
|
|
4
|
+
(:mod:`fundedops._async_client`) clients differ ONLY in ``await``: every path,
|
|
5
|
+
query param, request body, and response-parse decision lives here so the two
|
|
6
|
+
surfaces cannot drift (D-17 requires them to be first-class equals). Each public
|
|
7
|
+
client method maps to one :func:`RequestSpec` builder below plus one parser.
|
|
8
|
+
|
|
9
|
+
Security: the ``Authorization: Bearer fo_...`` header is built here (T-19-15 —
|
|
10
|
+
the default base URL is hardcoded ``https://`` so the key only ever leaves over
|
|
11
|
+
TLS unless the caller explicitly overrides ``base_url``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, NamedTuple
|
|
17
|
+
|
|
18
|
+
from .models import (
|
|
19
|
+
Account,
|
|
20
|
+
AccountInformation,
|
|
21
|
+
DealsPage,
|
|
22
|
+
OrdersPage,
|
|
23
|
+
PositionsPage,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DEFAULT_BASE_URL = "https://api.fundedops.com"
|
|
27
|
+
DEFAULT_TIMEOUT = 30.0
|
|
28
|
+
|
|
29
|
+
# The mounted public sub-app lives under /v1 (backend app.mount("/v1", public_app)).
|
|
30
|
+
_V1 = "/v1"
|
|
31
|
+
|
|
32
|
+
# Sentinel distinguishing "field omitted from a PATCH" from an explicit ``None``
|
|
33
|
+
# / empty value the caller wants to send (mirrors the backend's
|
|
34
|
+
# model_fields_set / exclude_unset semantics in UpdateAccountRequest).
|
|
35
|
+
UNSET: Any = object()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RequestSpec(NamedTuple):
|
|
39
|
+
"""A pure description of one HTTP call. The clients turn it into a real
|
|
40
|
+
request; nothing here touches the network, so it is trivially testable and
|
|
41
|
+
identical across sync and async."""
|
|
42
|
+
|
|
43
|
+
method: str
|
|
44
|
+
path: str
|
|
45
|
+
params: dict[str, Any] | None = None
|
|
46
|
+
json: dict[str, Any] | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_headers(api_key: str) -> dict[str, str]:
|
|
50
|
+
"""The confirmed public auth scheme (backend auth.py::_extract_key):
|
|
51
|
+
``Authorization: Bearer fo_...``."""
|
|
52
|
+
return {"Authorization": f"Bearer {api_key}"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _clean(mapping: dict[str, Any]) -> dict[str, Any]:
|
|
56
|
+
"""Drop keys whose value is ``None`` or the :data:`UNSET` sentinel — used for
|
|
57
|
+
optional query params and optional create fields."""
|
|
58
|
+
return {k: v for k, v in mapping.items() if v is not None and v is not UNSET}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ws_stream_url(base_url: str, api_key: str) -> str:
|
|
62
|
+
"""The ``wss://.../v1/stream?api_key=`` URL (backend ws_auth.py query auth).
|
|
63
|
+
|
|
64
|
+
http(s) → ws(s); the key rides the query string, so this string must NEVER be
|
|
65
|
+
logged (T-19-07 — enforced by the grep gate in the streaming module)."""
|
|
66
|
+
base = base_url.rstrip("/")
|
|
67
|
+
if base.startswith("https://"):
|
|
68
|
+
base = "wss://" + base[len("https://") :]
|
|
69
|
+
elif base.startswith("http://"):
|
|
70
|
+
base = "ws://" + base[len("http://") :]
|
|
71
|
+
return f"{base}{_V1}/stream?api_key={api_key}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# --- REST request-spec builders (one per public method) ---------------------
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def list_accounts_spec(
|
|
78
|
+
offset: int = 0,
|
|
79
|
+
limit: int | None = None,
|
|
80
|
+
state: str | None = None,
|
|
81
|
+
tags: str | None = None,
|
|
82
|
+
) -> RequestSpec:
|
|
83
|
+
params = _clean({"offset": offset, "limit": limit, "state": state, "tags": tags})
|
|
84
|
+
return RequestSpec("GET", f"{_V1}/accounts", params=params)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def create_account_spec(
|
|
88
|
+
login: str,
|
|
89
|
+
password: str,
|
|
90
|
+
server: str,
|
|
91
|
+
platform: str,
|
|
92
|
+
name: str | None = None,
|
|
93
|
+
tags: list[str] | None = None,
|
|
94
|
+
metadata: dict | None = None,
|
|
95
|
+
) -> RequestSpec:
|
|
96
|
+
body = _clean(
|
|
97
|
+
{
|
|
98
|
+
"login": login,
|
|
99
|
+
"password": password,
|
|
100
|
+
"server": server,
|
|
101
|
+
"platform": platform,
|
|
102
|
+
"name": name,
|
|
103
|
+
"tags": tags,
|
|
104
|
+
"metadata": metadata,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return RequestSpec("POST", f"{_V1}/accounts", json=body)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_account_spec(account_id: str) -> RequestSpec:
|
|
111
|
+
return RequestSpec("GET", f"{_V1}/accounts/{account_id}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def update_account_spec(
|
|
115
|
+
account_id: str,
|
|
116
|
+
name: Any = UNSET,
|
|
117
|
+
tags: Any = UNSET,
|
|
118
|
+
metadata: Any = UNSET,
|
|
119
|
+
) -> RequestSpec:
|
|
120
|
+
# Send only the fields the caller actually set (UNSET → omit); an explicit
|
|
121
|
+
# ``None``/``[]``/``{}`` is a real value and is forwarded.
|
|
122
|
+
body = {
|
|
123
|
+
k: v
|
|
124
|
+
for k, v in {"name": name, "tags": tags, "metadata": metadata}.items()
|
|
125
|
+
if v is not UNSET
|
|
126
|
+
}
|
|
127
|
+
return RequestSpec("PATCH", f"{_V1}/accounts/{account_id}", json=body)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def delete_account_spec(account_id: str) -> RequestSpec:
|
|
131
|
+
return RequestSpec("DELETE", f"{_V1}/accounts/{account_id}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def deploy_spec(account_id: str) -> RequestSpec:
|
|
135
|
+
return RequestSpec("POST", f"{_V1}/accounts/{account_id}/deploy")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def undeploy_spec(account_id: str) -> RequestSpec:
|
|
139
|
+
return RequestSpec("POST", f"{_V1}/accounts/{account_id}/undeploy")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def redeploy_spec(account_id: str) -> RequestSpec:
|
|
143
|
+
return RequestSpec("POST", f"{_V1}/accounts/{account_id}/redeploy")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def account_information_spec(account_id: str) -> RequestSpec:
|
|
147
|
+
return RequestSpec("GET", f"{_V1}/accounts/{account_id}/account-information")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def positions_spec(account_id: str) -> RequestSpec:
|
|
151
|
+
return RequestSpec("GET", f"{_V1}/accounts/{account_id}/positions")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def pending_orders_spec(account_id: str) -> RequestSpec:
|
|
155
|
+
return RequestSpec("GET", f"{_V1}/accounts/{account_id}/orders")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def history_deals_spec(
|
|
159
|
+
account_id: str,
|
|
160
|
+
start_time: str | None = None,
|
|
161
|
+
end_time: str | None = None,
|
|
162
|
+
ticket: str | None = None,
|
|
163
|
+
position: str | None = None,
|
|
164
|
+
offset: int = 0,
|
|
165
|
+
limit: int | None = None,
|
|
166
|
+
) -> RequestSpec:
|
|
167
|
+
params = _clean(
|
|
168
|
+
{
|
|
169
|
+
"start_time": start_time,
|
|
170
|
+
"end_time": end_time,
|
|
171
|
+
"ticket": ticket,
|
|
172
|
+
"position": position,
|
|
173
|
+
"offset": offset if offset else None,
|
|
174
|
+
"limit": limit,
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
return RequestSpec(
|
|
178
|
+
"GET", f"{_V1}/accounts/{account_id}/history-deals", params=params
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def history_orders_spec(
|
|
183
|
+
account_id: str,
|
|
184
|
+
start_time: str | None = None,
|
|
185
|
+
end_time: str | None = None,
|
|
186
|
+
ticket: str | None = None,
|
|
187
|
+
position: str | None = None,
|
|
188
|
+
offset: int = 0,
|
|
189
|
+
limit: int | None = None,
|
|
190
|
+
) -> RequestSpec:
|
|
191
|
+
params = _clean(
|
|
192
|
+
{
|
|
193
|
+
"start_time": start_time,
|
|
194
|
+
"end_time": end_time,
|
|
195
|
+
"ticket": ticket,
|
|
196
|
+
"position": position,
|
|
197
|
+
"offset": offset if offset else None,
|
|
198
|
+
"limit": limit,
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
return RequestSpec(
|
|
202
|
+
"GET", f"{_V1}/accounts/{account_id}/history-orders", params=params
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# --- Response parsers (shared so sync/async return identical types) ---------
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def parse_account(data: Any) -> Account:
|
|
210
|
+
return Account.model_validate(data)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def parse_accounts(data: Any) -> list[Account]:
|
|
214
|
+
return [Account.model_validate(row) for row in data]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def parse_account_information(data: Any) -> AccountInformation:
|
|
218
|
+
return AccountInformation.model_validate(data)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def parse_positions_page(data: Any) -> PositionsPage:
|
|
222
|
+
return PositionsPage.model_validate(data)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def parse_orders_page(data: Any) -> OrdersPage:
|
|
226
|
+
return OrdersPage.model_validate(data)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def parse_deals_page(data: Any) -> DealsPage:
|
|
230
|
+
return DealsPage.model_validate(data)
|