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.
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .env
5
+ .env.local
6
+ *.egg-info/
7
+ .venv/
8
+ venv/
9
+ dist/
10
+ build/
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+ node_modules/
14
+ .next/
15
+ frontend/.next/
16
+ backend/app/**/__pycache__/
17
+ .worktrees/
@@ -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()