fundedops 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.
- fundedops-0.1.0/.gitignore +17 -0
- fundedops-0.1.0/PKG-INFO +127 -0
- fundedops-0.1.0/README.md +98 -0
- fundedops-0.1.0/pyproject.toml +48 -0
- fundedops-0.1.0/src/fundedops/__init__.py +105 -0
- fundedops-0.1.0/src/fundedops/_async_client.py +222 -0
- fundedops-0.1.0/src/fundedops/_base.py +230 -0
- fundedops-0.1.0/src/fundedops/_client.py +237 -0
- fundedops-0.1.0/src/fundedops/_streaming.py +108 -0
- fundedops-0.1.0/src/fundedops/errors.py +157 -0
- fundedops-0.1.0/src/fundedops/models.py +266 -0
- fundedops-0.1.0/src/fundedops/py.typed +0 -0
- fundedops-0.1.0/tests/conftest.py +264 -0
- fundedops-0.1.0/tests/test_async_client.py +366 -0
- fundedops-0.1.0/tests/test_client.py +454 -0
- fundedops-0.1.0/tests/test_errors.py +141 -0
- fundedops-0.1.0/tests/test_models.py +324 -0
- fundedops-0.1.0/tests/test_streaming.py +176 -0
fundedops-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fundedops
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the FundedOps API — typed accounts, positions, orders, history, and real-time streaming.
|
|
5
|
+
Project-URL: Homepage, https://fundedops.com
|
|
6
|
+
Project-URL: Documentation, https://api.fundedops.com/docs
|
|
7
|
+
Project-URL: Source, https://github.com/fundedops/fundedops-python
|
|
8
|
+
Author: FundedOps
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: api,fundedops,metatrader,mt5,sdk,trading
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
24
|
+
Requires-Dist: websockets>=13
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# fundedops
|
|
31
|
+
|
|
32
|
+
The official Python SDK for the [FundedOps API](https://api.fundedops.com/docs) —
|
|
33
|
+
typed access to accounts, positions, orders, trade history, and real-time
|
|
34
|
+
streaming, backed by pydantic v2 models that mirror the wire format exactly.
|
|
35
|
+
|
|
36
|
+
> This file doubles as the package's [PyPI](https://pypi.org/project/fundedops/) page.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install fundedops
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requires Python 3.10+.
|
|
45
|
+
|
|
46
|
+
## Authentication
|
|
47
|
+
|
|
48
|
+
Every request carries your API key as a bearer token
|
|
49
|
+
(`Authorization: Bearer fo_...`). Create a key from the developer portal. The
|
|
50
|
+
key travels in the WebSocket URL's query string — never log stream URLs.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
Two first-class clients with an identical method surface — synchronous
|
|
55
|
+
`FundedOps` and asynchronous `AsyncFundedOps`. Every method lives under the
|
|
56
|
+
`client.accounts` namespace, except `wait_until_connected` and `stream`,
|
|
57
|
+
which are on the client itself.
|
|
58
|
+
|
|
59
|
+
### Synchronous
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from fundedops import FundedOps
|
|
63
|
+
|
|
64
|
+
with FundedOps(api_key="fo_live_...") as client:
|
|
65
|
+
for account in client.accounts.list():
|
|
66
|
+
print(account.id, account.state, account.connection_status)
|
|
67
|
+
|
|
68
|
+
info = client.accounts.account_information(account.id)
|
|
69
|
+
print(info.balance, info.equity, info.currency)
|
|
70
|
+
|
|
71
|
+
# Deploy and block until connected (timeout is mandatory).
|
|
72
|
+
client.accounts.deploy(account.id)
|
|
73
|
+
client.wait_until_connected(account.id, timeout=180)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The full lifecycle and read surface is available on `client.accounts`:
|
|
77
|
+
`create`, `get`, `list`, `update`, `delete`, `deploy`, `undeploy`,
|
|
78
|
+
`redeploy`, `account_information`, `positions`, `pending_orders`,
|
|
79
|
+
`history_deals`, and `history_orders`.
|
|
80
|
+
|
|
81
|
+
### Asynchronous
|
|
82
|
+
|
|
83
|
+
`AsyncFundedOps` is the exact awaitable mirror — same constructor, same
|
|
84
|
+
method names, same return types (a parity test enforces it):
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import asyncio
|
|
88
|
+
from fundedops import AsyncFundedOps
|
|
89
|
+
|
|
90
|
+
async def main():
|
|
91
|
+
async with AsyncFundedOps(api_key="fo_live_...") as client:
|
|
92
|
+
accounts = await client.accounts.list()
|
|
93
|
+
info = await client.accounts.account_information(accounts[0].id)
|
|
94
|
+
print(info.equity)
|
|
95
|
+
|
|
96
|
+
asyncio.run(main())
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Streaming
|
|
100
|
+
|
|
101
|
+
`client.stream([account_id, ...])` opens a WebSocket, subscribes each
|
|
102
|
+
account, answers server pings for you, and yields typed events:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from fundedops import FundedOps
|
|
106
|
+
|
|
107
|
+
client = FundedOps(api_key="fo_live_...")
|
|
108
|
+
for event in client.stream(["9b2c..."]):
|
|
109
|
+
if event.type == "positions_updated":
|
|
110
|
+
print("open:", len(event.updated), "closed:", event.removed_ids)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Errors
|
|
114
|
+
|
|
115
|
+
Every non-2xx response raises a typed exception derived from
|
|
116
|
+
`FundedOpsError` (`ValidationError`, `UnauthorizedError`,
|
|
117
|
+
`PaymentRequiredError`, `NotFoundError`, `RateLimitedError` with
|
|
118
|
+
`retry_after`, and more), each carrying `status`, `message`, and
|
|
119
|
+
`request_id`.
|
|
120
|
+
|
|
121
|
+
## Documentation
|
|
122
|
+
|
|
123
|
+
Full API reference and guides: https://api.fundedops.com/docs
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# fundedops
|
|
2
|
+
|
|
3
|
+
The official Python SDK for the [FundedOps API](https://api.fundedops.com/docs) —
|
|
4
|
+
typed access to accounts, positions, orders, trade history, and real-time
|
|
5
|
+
streaming, backed by pydantic v2 models that mirror the wire format exactly.
|
|
6
|
+
|
|
7
|
+
> This file doubles as the package's [PyPI](https://pypi.org/project/fundedops/) page.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install fundedops
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.10+.
|
|
16
|
+
|
|
17
|
+
## Authentication
|
|
18
|
+
|
|
19
|
+
Every request carries your API key as a bearer token
|
|
20
|
+
(`Authorization: Bearer fo_...`). Create a key from the developer portal. The
|
|
21
|
+
key travels in the WebSocket URL's query string — never log stream URLs.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Two first-class clients with an identical method surface — synchronous
|
|
26
|
+
`FundedOps` and asynchronous `AsyncFundedOps`. Every method lives under the
|
|
27
|
+
`client.accounts` namespace, except `wait_until_connected` and `stream`,
|
|
28
|
+
which are on the client itself.
|
|
29
|
+
|
|
30
|
+
### Synchronous
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from fundedops import FundedOps
|
|
34
|
+
|
|
35
|
+
with FundedOps(api_key="fo_live_...") as client:
|
|
36
|
+
for account in client.accounts.list():
|
|
37
|
+
print(account.id, account.state, account.connection_status)
|
|
38
|
+
|
|
39
|
+
info = client.accounts.account_information(account.id)
|
|
40
|
+
print(info.balance, info.equity, info.currency)
|
|
41
|
+
|
|
42
|
+
# Deploy and block until connected (timeout is mandatory).
|
|
43
|
+
client.accounts.deploy(account.id)
|
|
44
|
+
client.wait_until_connected(account.id, timeout=180)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The full lifecycle and read surface is available on `client.accounts`:
|
|
48
|
+
`create`, `get`, `list`, `update`, `delete`, `deploy`, `undeploy`,
|
|
49
|
+
`redeploy`, `account_information`, `positions`, `pending_orders`,
|
|
50
|
+
`history_deals`, and `history_orders`.
|
|
51
|
+
|
|
52
|
+
### Asynchronous
|
|
53
|
+
|
|
54
|
+
`AsyncFundedOps` is the exact awaitable mirror — same constructor, same
|
|
55
|
+
method names, same return types (a parity test enforces it):
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import asyncio
|
|
59
|
+
from fundedops import AsyncFundedOps
|
|
60
|
+
|
|
61
|
+
async def main():
|
|
62
|
+
async with AsyncFundedOps(api_key="fo_live_...") as client:
|
|
63
|
+
accounts = await client.accounts.list()
|
|
64
|
+
info = await client.accounts.account_information(accounts[0].id)
|
|
65
|
+
print(info.equity)
|
|
66
|
+
|
|
67
|
+
asyncio.run(main())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Streaming
|
|
71
|
+
|
|
72
|
+
`client.stream([account_id, ...])` opens a WebSocket, subscribes each
|
|
73
|
+
account, answers server pings for you, and yields typed events:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from fundedops import FundedOps
|
|
77
|
+
|
|
78
|
+
client = FundedOps(api_key="fo_live_...")
|
|
79
|
+
for event in client.stream(["9b2c..."]):
|
|
80
|
+
if event.type == "positions_updated":
|
|
81
|
+
print("open:", len(event.updated), "closed:", event.removed_ids)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Errors
|
|
85
|
+
|
|
86
|
+
Every non-2xx response raises a typed exception derived from
|
|
87
|
+
`FundedOpsError` (`ValidationError`, `UnauthorizedError`,
|
|
88
|
+
`PaymentRequiredError`, `NotFoundError`, `RateLimitedError` with
|
|
89
|
+
`retry_after`, and more), each carrying `status`, `message`, and
|
|
90
|
+
`request_id`.
|
|
91
|
+
|
|
92
|
+
## Documentation
|
|
93
|
+
|
|
94
|
+
Full API reference and guides: https://api.fundedops.com/docs
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fundedops"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for the FundedOps API — typed accounts, positions, orders, history, and real-time streaming."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "FundedOps" }]
|
|
13
|
+
keywords = ["fundedops", "trading", "mt5", "metatrader", "api", "sdk"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.27",
|
|
28
|
+
"websockets>=13",
|
|
29
|
+
"pydantic>=2.7,<3",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0.0",
|
|
35
|
+
"pytest-asyncio>=0.23.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://fundedops.com"
|
|
40
|
+
Documentation = "https://api.fundedops.com/docs"
|
|
41
|
+
Source = "https://github.com/fundedops/fundedops-python"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/fundedops"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
# Async client + streaming tests are plain `async def` — no per-test decorator.
|
|
48
|
+
asyncio_mode = "auto"
|
|
@@ -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()
|