ctrader-api-client 0.5.0__tar.gz → 0.6.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.
Potentially problematic release.
This version of ctrader-api-client might be problematic. Click here for more details.
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/.gitignore +4 -1
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/PKG-INFO +31 -4
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/README.md +30 -3
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/docs/api/client.md +13 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/docs/api/events.md +9 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/pyproject.toml +27 -1
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/_internal/__init__.py +11 -4
- ctrader_api_client-0.6.0/src/ctrader_api_client/_internal/clock.py +36 -0
- ctrader_api_client-0.6.0/src/ctrader_api_client/_internal/conversions.py +41 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/_internal/messages.py +13 -43
- ctrader_api_client-0.6.0/src/ctrader_api_client/api/_base.py +25 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/api/accounts.py +5 -26
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/api/market_data.py +26 -81
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/api/symbols.py +8 -34
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/api/trading.py +57 -199
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/auth/__init__.py +3 -0
- ctrader_api_client-0.6.0/src/ctrader_api_client/auth/_session.py +59 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/auth/manager.py +234 -138
- ctrader_api_client-0.6.0/src/ctrader_api_client/auth/policy.py +47 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/client.py +42 -3
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/config.py +8 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/connection/heartbeat.py +21 -7
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/connection/protocol.py +171 -19
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/events/__init__.py +2 -0
- ctrader_api_client-0.6.0/src/ctrader_api_client/events/_execution.py +92 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/events/emitter.py +49 -30
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/events/router.py +17 -76
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/events/types.py +21 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/models/account.py +6 -11
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/models/deal.py +7 -13
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/models/order.py +23 -16
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/models/position.py +20 -19
- ctrader_api_client-0.6.0/tests/__init__.py +1 -0
- ctrader_api_client-0.6.0/tests/behavior/__init__.py +1 -0
- ctrader_api_client-0.6.0/tests/behavior/api/__init__.py +1 -0
- ctrader_api_client-0.6.0/tests/behavior/api/conftest.py +29 -0
- ctrader_api_client-0.6.0/tests/behavior/api/test_market_data.py +348 -0
- ctrader_api_client-0.6.0/tests/behavior/api/test_reference_data.py +196 -0
- ctrader_api_client-0.6.0/tests/behavior/api/test_trading.py +492 -0
- ctrader_api_client-0.6.0/tests/behavior/auth/__init__.py +1 -0
- ctrader_api_client-0.6.0/tests/behavior/auth/conftest.py +52 -0
- ctrader_api_client-0.6.0/tests/behavior/auth/test_authentication.py +355 -0
- ctrader_api_client-0.6.0/tests/behavior/auth/test_token_lifecycle.py +345 -0
- ctrader_api_client-0.6.0/tests/behavior/connection/__init__.py +1 -0
- ctrader_api_client-0.6.0/tests/behavior/connection/test_event_dispatch.py +141 -0
- ctrader_api_client-0.6.0/tests/behavior/connection/test_framing.py +116 -0
- ctrader_api_client-0.6.0/tests/behavior/connection/test_heartbeat.py +99 -0
- ctrader_api_client-0.6.0/tests/behavior/connection/test_reconnection.py +118 -0
- ctrader_api_client-0.6.0/tests/behavior/connection/test_request_response.py +107 -0
- ctrader_api_client-0.6.0/tests/behavior/events/__init__.py +1 -0
- ctrader_api_client-0.6.0/tests/behavior/events/conftest.py +35 -0
- ctrader_api_client-0.6.0/tests/behavior/events/test_routing.py +622 -0
- ctrader_api_client-0.6.0/tests/behavior/events/test_subscriptions.py +365 -0
- ctrader_api_client-0.6.0/tests/behavior/test_client.py +375 -0
- ctrader_api_client-0.6.0/tests/conftest.py +102 -0
- ctrader_api_client-0.6.0/tests/harness/__init__.py +33 -0
- ctrader_api_client-0.6.0/tests/harness/clock.py +99 -0
- ctrader_api_client-0.6.0/tests/harness/factories.py +130 -0
- ctrader_api_client-0.6.0/tests/harness/recorder.py +73 -0
- ctrader_api_client-0.6.0/tests/harness/server.py +268 -0
- ctrader_api_client-0.6.0/tests/harness/signals.py +58 -0
- ctrader_api_client-0.6.0/tests/harness/stub_protocol.py +158 -0
- ctrader_api_client-0.6.0/tests/harness/wire.py +94 -0
- ctrader_api_client-0.6.0/tests/unit/__init__.py +1 -0
- ctrader_api_client-0.6.0/tests/unit/test_api_errors.py +71 -0
- ctrader_api_client-0.6.0/tests/unit/test_market_data_conversion.py +127 -0
- ctrader_api_client-0.6.0/tests/unit/test_request_conversion.py +221 -0
- ctrader_api_client-0.6.0/tests/unit/test_symbol_conversion.py +144 -0
- ctrader_api_client-0.6.0/tests/unit/test_trading_records.py +302 -0
- ctrader_api_client-0.6.0/tests/unit/test_wire_format.py +199 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/uv.lock +100 -1
- ctrader_api_client-0.5.0/tests/unit/_internal/test_messages.py +0 -270
- ctrader_api_client-0.5.0/tests/unit/_internal/test_serialization.py +0 -172
- ctrader_api_client-0.5.0/tests/unit/api/conftest.py +0 -13
- ctrader_api_client-0.5.0/tests/unit/api/test_accounts.py +0 -116
- ctrader_api_client-0.5.0/tests/unit/api/test_market_data_api.py +0 -220
- ctrader_api_client-0.5.0/tests/unit/api/test_symbols.py +0 -281
- ctrader_api_client-0.5.0/tests/unit/api/test_trading.py +0 -434
- ctrader_api_client-0.5.0/tests/unit/auth/test_credentials.py +0 -177
- ctrader_api_client-0.5.0/tests/unit/auth/test_manager.py +0 -962
- ctrader_api_client-0.5.0/tests/unit/connection/test_heartbeat.py +0 -219
- ctrader_api_client-0.5.0/tests/unit/connection/test_protocol.py +0 -419
- ctrader_api_client-0.5.0/tests/unit/connection/test_transport.py +0 -244
- ctrader_api_client-0.5.0/tests/unit/events/test_emitter.py +0 -352
- ctrader_api_client-0.5.0/tests/unit/events/test_router.py +0 -581
- ctrader_api_client-0.5.0/tests/unit/events/test_types.py +0 -320
- ctrader_api_client-0.5.0/tests/unit/models/test_account.py +0 -204
- ctrader_api_client-0.5.0/tests/unit/models/test_deal.py +0 -244
- ctrader_api_client-0.5.0/tests/unit/models/test_market_data.py +0 -159
- ctrader_api_client-0.5.0/tests/unit/models/test_order.py +0 -310
- ctrader_api_client-0.5.0/tests/unit/models/test_position.py +0 -175
- ctrader_api_client-0.5.0/tests/unit/models/test_requests.py +0 -380
- ctrader_api_client-0.5.0/tests/unit/models/test_symbol.py +0 -236
- ctrader_api_client-0.5.0/tests/unit/test_client.py +0 -549
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/.claude/settings.local.json +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/.github/workflows/docs.yml +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/.pre-commit-config.yaml +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/.python-version +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/Justfile +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/LICENSE +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/docs/api/accounts.md +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/docs/api/enums.md +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/docs/api/market-data.md +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/docs/api/models.md +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/docs/api/symbols.md +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/docs/api/trading.md +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/docs/getting-started.md +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/docs/index.md +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/mkdocs.yml +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/protos/SOURCE +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/protos/VERSION +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/protos/update.sh +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/protos/vendor/OpenApiMessages.proto +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/protos/vendor/OpenApiModelMessages.proto +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/scripts/fix_proto_imports.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/__init__.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/api/__init__.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/auth/credentials.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/auth/trigger.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/connection/__init__.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/connection/transport.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/enums.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/exceptions.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/models/__init__.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/models/_base.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/models/market_data.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/models/requests.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/models/symbol.py +0 -0
- {ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctrader-api-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: API Client to interact with the cTrader Open API spec
|
|
5
5
|
Author-email: Elio <elioachukri@pm.me>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -119,7 +119,9 @@ creds = await client.auth.authenticate_by_trader_login(
|
|
|
119
119
|
expires_at=1778617423,
|
|
120
120
|
)
|
|
121
121
|
|
|
122
|
-
# Tokens are automatically refreshed before expiry
|
|
122
|
+
# Tokens are automatically refreshed before expiry. A refresh that fails is
|
|
123
|
+
# retried on the next check rather than dropping the session, and surfaces as a
|
|
124
|
+
# TokenRefreshFailedEvent so a persistently dead refresh token is observable.
|
|
123
125
|
```
|
|
124
126
|
|
|
125
127
|
### Market Data
|
|
@@ -150,10 +152,10 @@ request = NewOrderRequest(
|
|
|
150
152
|
side=OrderSide.BUY,
|
|
151
153
|
volume=100000, # 1 lot in cents
|
|
152
154
|
)
|
|
153
|
-
result = await client.trading.
|
|
155
|
+
result = await client.trading.place_order(account_id, request)
|
|
154
156
|
|
|
155
157
|
# Get open positions
|
|
156
|
-
positions = await client.trading.
|
|
158
|
+
positions = await client.trading.get_open_positions(account_id)
|
|
157
159
|
|
|
158
160
|
# Close a position
|
|
159
161
|
close_position = ClosePositionRequest(
|
|
@@ -171,6 +173,7 @@ from ctrader_api_client.events import (
|
|
|
171
173
|
ExecutionEvent,
|
|
172
174
|
ReadyEvent,
|
|
173
175
|
ReconnectedEvent,
|
|
176
|
+
TokenRefreshFailedEvent,
|
|
174
177
|
)
|
|
175
178
|
|
|
176
179
|
# Price updates
|
|
@@ -193,6 +196,12 @@ async def on_ready(event: ReadyEvent):
|
|
|
193
196
|
@client.on(ReconnectedEvent)
|
|
194
197
|
async def on_reconnected(event: ReconnectedEvent):
|
|
195
198
|
print(f"Reconnected, restored accounts: {event.restored_accounts}")
|
|
199
|
+
|
|
200
|
+
# Token refresh failed (retried automatically; a repeating event means the
|
|
201
|
+
# refresh token is no longer usable and the account must be re-authorized)
|
|
202
|
+
@client.on(TokenRefreshFailedEvent, account_id=account_id)
|
|
203
|
+
async def on_refresh_failed(event: TokenRefreshFailedEvent):
|
|
204
|
+
print(f"Token refresh failed for {event.account_id}: {event.error}")
|
|
196
205
|
```
|
|
197
206
|
|
|
198
207
|
### Symbols
|
|
@@ -237,6 +246,9 @@ Use `ReadyEvent` to set up subscriptions that persist across reconnections.
|
|
|
237
246
|
## Configuration
|
|
238
247
|
|
|
239
248
|
```python
|
|
249
|
+
from ctrader_api_client import ClientConfig
|
|
250
|
+
from ctrader_api_client.auth import ReauthPolicy, RefreshPolicy
|
|
251
|
+
|
|
240
252
|
config = ClientConfig(
|
|
241
253
|
client_id="your_client_id",
|
|
242
254
|
client_secret="your_client_secret",
|
|
@@ -255,5 +267,20 @@ config = ClientConfig(
|
|
|
255
267
|
reconnect_attempts=5,
|
|
256
268
|
reconnect_min_wait=1.0,
|
|
257
269
|
reconnect_max_wait=60.0,
|
|
270
|
+
|
|
271
|
+
# Token refresh: when to refresh access tokens and how hard to retry
|
|
272
|
+
refresh_policy=RefreshPolicy(
|
|
273
|
+
buffer_seconds=300.0, # refresh this long before expiry
|
|
274
|
+
check_interval=60.0, # how often to check for expiring tokens
|
|
275
|
+
retry_attempts=3,
|
|
276
|
+
retry_min_wait=1.0,
|
|
277
|
+
retry_max_wait=30.0,
|
|
278
|
+
),
|
|
279
|
+
|
|
280
|
+
# Session recovery: backoff for re-establishing sessions the server dropped
|
|
281
|
+
reauth_policy=ReauthPolicy(
|
|
282
|
+
min_wait=1.0,
|
|
283
|
+
max_wait=60.0,
|
|
284
|
+
),
|
|
258
285
|
)
|
|
259
286
|
```
|
|
@@ -100,7 +100,9 @@ creds = await client.auth.authenticate_by_trader_login(
|
|
|
100
100
|
expires_at=1778617423,
|
|
101
101
|
)
|
|
102
102
|
|
|
103
|
-
# Tokens are automatically refreshed before expiry
|
|
103
|
+
# Tokens are automatically refreshed before expiry. A refresh that fails is
|
|
104
|
+
# retried on the next check rather than dropping the session, and surfaces as a
|
|
105
|
+
# TokenRefreshFailedEvent so a persistently dead refresh token is observable.
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
### Market Data
|
|
@@ -131,10 +133,10 @@ request = NewOrderRequest(
|
|
|
131
133
|
side=OrderSide.BUY,
|
|
132
134
|
volume=100000, # 1 lot in cents
|
|
133
135
|
)
|
|
134
|
-
result = await client.trading.
|
|
136
|
+
result = await client.trading.place_order(account_id, request)
|
|
135
137
|
|
|
136
138
|
# Get open positions
|
|
137
|
-
positions = await client.trading.
|
|
139
|
+
positions = await client.trading.get_open_positions(account_id)
|
|
138
140
|
|
|
139
141
|
# Close a position
|
|
140
142
|
close_position = ClosePositionRequest(
|
|
@@ -152,6 +154,7 @@ from ctrader_api_client.events import (
|
|
|
152
154
|
ExecutionEvent,
|
|
153
155
|
ReadyEvent,
|
|
154
156
|
ReconnectedEvent,
|
|
157
|
+
TokenRefreshFailedEvent,
|
|
155
158
|
)
|
|
156
159
|
|
|
157
160
|
# Price updates
|
|
@@ -174,6 +177,12 @@ async def on_ready(event: ReadyEvent):
|
|
|
174
177
|
@client.on(ReconnectedEvent)
|
|
175
178
|
async def on_reconnected(event: ReconnectedEvent):
|
|
176
179
|
print(f"Reconnected, restored accounts: {event.restored_accounts}")
|
|
180
|
+
|
|
181
|
+
# Token refresh failed (retried automatically; a repeating event means the
|
|
182
|
+
# refresh token is no longer usable and the account must be re-authorized)
|
|
183
|
+
@client.on(TokenRefreshFailedEvent, account_id=account_id)
|
|
184
|
+
async def on_refresh_failed(event: TokenRefreshFailedEvent):
|
|
185
|
+
print(f"Token refresh failed for {event.account_id}: {event.error}")
|
|
177
186
|
```
|
|
178
187
|
|
|
179
188
|
### Symbols
|
|
@@ -218,6 +227,9 @@ Use `ReadyEvent` to set up subscriptions that persist across reconnections.
|
|
|
218
227
|
## Configuration
|
|
219
228
|
|
|
220
229
|
```python
|
|
230
|
+
from ctrader_api_client import ClientConfig
|
|
231
|
+
from ctrader_api_client.auth import ReauthPolicy, RefreshPolicy
|
|
232
|
+
|
|
221
233
|
config = ClientConfig(
|
|
222
234
|
client_id="your_client_id",
|
|
223
235
|
client_secret="your_client_secret",
|
|
@@ -236,5 +248,20 @@ config = ClientConfig(
|
|
|
236
248
|
reconnect_attempts=5,
|
|
237
249
|
reconnect_min_wait=1.0,
|
|
238
250
|
reconnect_max_wait=60.0,
|
|
251
|
+
|
|
252
|
+
# Token refresh: when to refresh access tokens and how hard to retry
|
|
253
|
+
refresh_policy=RefreshPolicy(
|
|
254
|
+
buffer_seconds=300.0, # refresh this long before expiry
|
|
255
|
+
check_interval=60.0, # how often to check for expiring tokens
|
|
256
|
+
retry_attempts=3,
|
|
257
|
+
retry_min_wait=1.0,
|
|
258
|
+
retry_max_wait=30.0,
|
|
259
|
+
),
|
|
260
|
+
|
|
261
|
+
# Session recovery: backoff for re-establishing sessions the server dropped
|
|
262
|
+
reauth_policy=ReauthPolicy(
|
|
263
|
+
min_wait=1.0,
|
|
264
|
+
max_wait=60.0,
|
|
265
|
+
),
|
|
239
266
|
)
|
|
240
267
|
```
|
|
@@ -27,6 +27,19 @@ The main entry point for interacting with the cTrader API.
|
|
|
27
27
|
options:
|
|
28
28
|
show_source: false
|
|
29
29
|
|
|
30
|
+
## Authentication Policies
|
|
31
|
+
|
|
32
|
+
Token-refresh and session-recovery timing, passed via
|
|
33
|
+
`ClientConfig(refresh_policy=..., reauth_policy=...)`.
|
|
34
|
+
|
|
35
|
+
::: ctrader_api_client.auth.RefreshPolicy
|
|
36
|
+
options:
|
|
37
|
+
show_source: false
|
|
38
|
+
|
|
39
|
+
::: ctrader_api_client.auth.ReauthPolicy
|
|
40
|
+
options:
|
|
41
|
+
show_source: false
|
|
42
|
+
|
|
30
43
|
## Authentication
|
|
31
44
|
|
|
32
45
|
The `client.auth` property provides access to authentication operations.
|
|
@@ -158,6 +158,15 @@ drop. Check current authorization with `client.is_account_authorized(account_id)
|
|
|
158
158
|
options:
|
|
159
159
|
show_source: false
|
|
160
160
|
|
|
161
|
+
::: ctrader_api_client.events.TokenRefreshFailedEvent
|
|
162
|
+
options:
|
|
163
|
+
show_source: false
|
|
164
|
+
|
|
165
|
+
The client keeps the existing credentials and retries on the next refresh check,
|
|
166
|
+
so a single event usually means a transient outage. If the event keeps repeating,
|
|
167
|
+
the refresh token is no longer usable and the account has to be re-authorized out
|
|
168
|
+
of band.
|
|
169
|
+
|
|
161
170
|
## Symbol Events
|
|
162
171
|
|
|
163
172
|
::: ctrader_api_client.events.SymbolChangedEvent
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ctrader-api-client"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "API Client to interact with the cTrader Open API spec"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -32,10 +32,36 @@ dev = [
|
|
|
32
32
|
"mkdocs-material>=9.7.6",
|
|
33
33
|
"mkdocstrings[python]>=1.0.3",
|
|
34
34
|
"pytest>=9.0.3",
|
|
35
|
+
"pytest-cov>=7.0.0",
|
|
36
|
+
"pytest-timeout>=2.4.0",
|
|
35
37
|
"ruff>=0.15.9",
|
|
36
38
|
"zuban>=0.7.0",
|
|
37
39
|
]
|
|
38
40
|
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
addopts = "--strict-markers --strict-config"
|
|
44
|
+
# Every coroutine test runs under anyio without a per-test marker. Marking them
|
|
45
|
+
# later (in a collection hook) makes anyio rebuild the item and silently drop
|
|
46
|
+
# any pytest.mark.parametrize arguments.
|
|
47
|
+
anyio_mode = "auto"
|
|
48
|
+
# No test should ever hang: a stuck reader loop or an unresolved request must
|
|
49
|
+
# fail the run rather than block it.
|
|
50
|
+
timeout = 30
|
|
51
|
+
filterwarnings = [
|
|
52
|
+
"error",
|
|
53
|
+
# stringcase, a transitive betterproto dependency, is unmaintained and has
|
|
54
|
+
# invalid escape sequences that newer Pythons flag on import.
|
|
55
|
+
"ignore::SyntaxWarning",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.coverage.run]
|
|
59
|
+
source = ["ctrader_api_client"]
|
|
60
|
+
omit = ["*/_internal/proto/*"]
|
|
61
|
+
|
|
62
|
+
[tool.coverage.report]
|
|
63
|
+
exclude_also = ["if TYPE_CHECKING:", "@overload", "\\.\\.\\."]
|
|
64
|
+
|
|
39
65
|
[tool.ruff]
|
|
40
66
|
line-length = 120
|
|
41
67
|
|
{ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/_internal/__init__.py
RENAMED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
from .clock import Clock, MonotonicClock
|
|
2
|
+
from .conversions import DEFAULT_MONEY_DIGITS, money_divisor, timestamp_to_datetime
|
|
1
3
|
from .messages import (
|
|
2
4
|
ClientMessageIdGenerator,
|
|
3
|
-
MessageRegistry,
|
|
4
5
|
deserialize_proto_message,
|
|
5
|
-
|
|
6
|
+
get_class,
|
|
7
|
+
get_payload_type,
|
|
6
8
|
unwrap_message,
|
|
7
9
|
wrap_message,
|
|
8
10
|
)
|
|
@@ -14,13 +16,18 @@ from .serialization import (
|
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
__all__ = [
|
|
19
|
+
"DEFAULT_MONEY_DIGITS",
|
|
17
20
|
"ClientMessageIdGenerator",
|
|
18
|
-
"
|
|
21
|
+
"Clock",
|
|
22
|
+
"MonotonicClock",
|
|
19
23
|
"deserialize_proto_message",
|
|
20
24
|
"encode_with_length_prefix",
|
|
21
|
-
"
|
|
25
|
+
"get_class",
|
|
26
|
+
"get_payload_type",
|
|
27
|
+
"money_divisor",
|
|
22
28
|
"read_exact",
|
|
23
29
|
"read_framed_message",
|
|
30
|
+
"timestamp_to_datetime",
|
|
24
31
|
"unwrap_message",
|
|
25
32
|
"wrap_message",
|
|
26
33
|
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Time source abstraction for timer-driven components."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Protocol as TypingProtocol
|
|
7
|
+
|
|
8
|
+
import anyio
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Clock(TypingProtocol):
|
|
12
|
+
"""A source of monotonic time and delays.
|
|
13
|
+
|
|
14
|
+
Injected into components whose behaviour is driven by elapsed time so that
|
|
15
|
+
those timings can be controlled deterministically instead of waited on.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def now(self) -> float:
|
|
19
|
+
"""Return the current value of a monotonic clock, in seconds."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
async def sleep(self, seconds: float) -> None:
|
|
23
|
+
"""Suspend the calling task for the given number of seconds."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MonotonicClock:
|
|
28
|
+
"""Default clock backed by :func:`time.monotonic` and :func:`anyio.sleep`."""
|
|
29
|
+
|
|
30
|
+
def now(self) -> float:
|
|
31
|
+
"""Return the current monotonic time in seconds."""
|
|
32
|
+
return time.monotonic()
|
|
33
|
+
|
|
34
|
+
async def sleep(self, seconds: float) -> None:
|
|
35
|
+
"""Suspend the calling task for the given number of seconds."""
|
|
36
|
+
await anyio.sleep(seconds)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Conversions between wire representations and Python types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# The scale the server uses for monetary amounts when it does not say otherwise.
|
|
9
|
+
# money_digits is a plain proto3 scalar, so a message that omits it parses as 0,
|
|
10
|
+
# which would scale by 1 and overstate every amount by two orders of magnitude.
|
|
11
|
+
DEFAULT_MONEY_DIGITS = 2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def money_divisor(money_digits: int) -> int:
|
|
15
|
+
"""Return the divisor that turns a scaled money integer into its real value.
|
|
16
|
+
|
|
17
|
+
Monetary amounts arrive as integers scaled by ``10 ** money_digits``. A
|
|
18
|
+
``money_digits`` of 0 is read as absent rather than as "no scaling", because
|
|
19
|
+
the wire format cannot distinguish the two and no account quotes money in
|
|
20
|
+
whole units.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
money_digits: The exponent reported alongside the amount, or 0 if the
|
|
24
|
+
server omitted it.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
The divisor to apply to the scaled integer.
|
|
28
|
+
"""
|
|
29
|
+
return 10 ** (money_digits or DEFAULT_MONEY_DIGITS)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def timestamp_to_datetime(timestamp_ms: int) -> datetime:
|
|
33
|
+
"""Convert a millisecond UTC timestamp to an aware datetime.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
timestamp_ms: Milliseconds since the Unix epoch, as sent by the server.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The equivalent timezone-aware datetime in UTC.
|
|
40
|
+
"""
|
|
41
|
+
return datetime.fromtimestamp(timestamp_ms / 1000, tz=UTC)
|
{ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/_internal/messages.py
RENAMED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import threading
|
|
6
|
-
from dataclasses import dataclass, field
|
|
7
6
|
|
|
8
7
|
import betterproto
|
|
9
8
|
|
|
@@ -205,25 +204,21 @@ _PAYLOAD_TYPE_TO_CLASS: dict[int, type[betterproto.Message]] = {
|
|
|
205
204
|
}
|
|
206
205
|
|
|
207
206
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
# The reverse direction, for wrapping outbound messages. Every class above maps
|
|
208
|
+
# from exactly one payload type, so inverting the mapping loses nothing.
|
|
209
|
+
_CLASS_TO_PAYLOAD_TYPE: dict[type[betterproto.Message], int] = {
|
|
210
|
+
cls: payload_type for payload_type, cls in _PAYLOAD_TYPE_TO_CLASS.items()
|
|
211
|
+
}
|
|
211
212
|
|
|
212
|
-
payload_type_to_class: dict[int, type[betterproto.Message]] = field(default_factory=dict)
|
|
213
|
-
class_to_payload_type: dict[type[betterproto.Message], int] = field(default_factory=dict)
|
|
214
213
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
def get_class(payload_type: int) -> type[betterproto.Message] | None:
|
|
215
|
+
"""Get the message class for a payload type, or None if unregistered."""
|
|
216
|
+
return _PAYLOAD_TYPE_TO_CLASS.get(payload_type)
|
|
218
217
|
|
|
219
|
-
def get_payload_type(self, cls: type[betterproto.Message]) -> int | None:
|
|
220
|
-
"""Get the payload type for a message class."""
|
|
221
|
-
return self.class_to_payload_type.get(cls)
|
|
222
218
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
self.class_to_payload_type[cls] = payload_type
|
|
219
|
+
def get_payload_type(cls: type[betterproto.Message]) -> int | None:
|
|
220
|
+
"""Get the payload type for a message class, or None if unregistered."""
|
|
221
|
+
return _CLASS_TO_PAYLOAD_TYPE.get(cls)
|
|
227
222
|
|
|
228
223
|
|
|
229
224
|
class ClientMessageIdGenerator:
|
|
@@ -240,29 +235,6 @@ class ClientMessageIdGenerator:
|
|
|
240
235
|
return str(self._counter)
|
|
241
236
|
|
|
242
237
|
|
|
243
|
-
# Module-level singleton
|
|
244
|
-
_registry: MessageRegistry | None = None
|
|
245
|
-
_registry_lock = threading.Lock()
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def _build_registry() -> MessageRegistry:
|
|
249
|
-
"""Build the message registry from the explicit mapping."""
|
|
250
|
-
registry = MessageRegistry()
|
|
251
|
-
for payload_type, cls in _PAYLOAD_TYPE_TO_CLASS.items():
|
|
252
|
-
registry.register(payload_type, cls)
|
|
253
|
-
return registry
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def get_registry() -> MessageRegistry:
|
|
257
|
-
"""Get the lazily-initialized singleton registry."""
|
|
258
|
-
global _registry
|
|
259
|
-
if _registry is None:
|
|
260
|
-
with _registry_lock:
|
|
261
|
-
if _registry is None:
|
|
262
|
-
_registry = _build_registry()
|
|
263
|
-
return _registry
|
|
264
|
-
|
|
265
|
-
|
|
266
238
|
def wrap_message(inner: betterproto.Message, client_msg_id: str | None = None) -> ProtoMessage:
|
|
267
239
|
"""Wrap an inner message into a ProtoMessage for transmission.
|
|
268
240
|
|
|
@@ -276,8 +248,7 @@ def wrap_message(inner: betterproto.Message, client_msg_id: str | None = None) -
|
|
|
276
248
|
Raises:
|
|
277
249
|
UnknownPayloadTypeError: If the inner message type is not registered.
|
|
278
250
|
"""
|
|
279
|
-
|
|
280
|
-
payload_type = registry.get_payload_type(type(inner))
|
|
251
|
+
payload_type = get_payload_type(type(inner))
|
|
281
252
|
|
|
282
253
|
if payload_type is None:
|
|
283
254
|
raise UnknownPayloadTypeError(payload_type=-1)
|
|
@@ -302,8 +273,7 @@ def unwrap_message(proto_message: ProtoMessage) -> betterproto.Message:
|
|
|
302
273
|
UnknownPayloadTypeError: If the payload type is not registered.
|
|
303
274
|
DeserializationError: If deserialization fails.
|
|
304
275
|
"""
|
|
305
|
-
|
|
306
|
-
cls = registry.get_class(proto_message.payload_type)
|
|
276
|
+
cls = get_class(proto_message.payload_type)
|
|
307
277
|
|
|
308
278
|
if cls is None:
|
|
309
279
|
raise UnknownPayloadTypeError(proto_message.payload_type)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ..connection import Protocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseAPI:
|
|
11
|
+
"""Shared construction and timeout handling for API namespaces."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, protocol: Protocol, default_timeout: float = 30.0) -> None:
|
|
14
|
+
"""Initialize the API namespace.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
protocol: The protocol instance for sending requests.
|
|
18
|
+
default_timeout: Default request timeout in seconds.
|
|
19
|
+
"""
|
|
20
|
+
self._protocol = protocol
|
|
21
|
+
self._default_timeout = default_timeout
|
|
22
|
+
|
|
23
|
+
def _timeout(self, timeout: float | None) -> float:
|
|
24
|
+
"""Resolve a per-call timeout against the configured default."""
|
|
25
|
+
return timeout or self._default_timeout
|
{ctrader_api_client-0.5.0 → ctrader_api_client-0.6.0}/src/ctrader_api_client/api/accounts.py
RENAMED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
|
|
5
3
|
from .._internal.proto import (
|
|
6
4
|
ProtoOATraderReq,
|
|
7
5
|
ProtoOATraderRes,
|
|
8
6
|
)
|
|
9
|
-
from ..exceptions import APIError
|
|
10
7
|
from ..models import Account
|
|
8
|
+
from ._base import BaseAPI
|
|
11
9
|
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
from ..connection import Protocol
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class AccountsAPI:
|
|
11
|
+
class AccountsAPI(BaseAPI):
|
|
18
12
|
"""Account information operations.
|
|
19
13
|
|
|
20
14
|
Provides methods to retrieve account/trader details.
|
|
@@ -27,16 +21,6 @@ class AccountsAPI:
|
|
|
27
21
|
```
|
|
28
22
|
"""
|
|
29
23
|
|
|
30
|
-
def __init__(self, protocol: Protocol, default_timeout: float = 30.0) -> None:
|
|
31
|
-
"""Initialize the accounts API.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
protocol: The protocol instance for sending requests.
|
|
35
|
-
default_timeout: Default request timeout in seconds.
|
|
36
|
-
"""
|
|
37
|
-
self._protocol = protocol
|
|
38
|
-
self._default_timeout = default_timeout
|
|
39
|
-
|
|
40
24
|
async def get_trader(
|
|
41
25
|
self,
|
|
42
26
|
account_id: int,
|
|
@@ -57,15 +41,10 @@ class AccountsAPI:
|
|
|
57
41
|
"""
|
|
58
42
|
request = ProtoOATraderReq(ctid_trader_account_id=account_id)
|
|
59
43
|
|
|
60
|
-
response = await self._protocol.
|
|
44
|
+
response = await self._protocol.request(
|
|
61
45
|
request,
|
|
62
|
-
|
|
46
|
+
ProtoOATraderRes,
|
|
47
|
+
timeout=self._timeout(timeout),
|
|
63
48
|
)
|
|
64
49
|
|
|
65
|
-
if not isinstance(response, ProtoOATraderRes):
|
|
66
|
-
raise APIError(
|
|
67
|
-
error_code="UNEXPECTED_RESPONSE",
|
|
68
|
-
description=f"Expected ProtoOATraderRes, got {type(response).__name__}",
|
|
69
|
-
)
|
|
70
|
-
|
|
71
50
|
return Account.from_proto(response.trader)
|