ctrader-api-client 0.10.0__tar.gz → 0.12.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.
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/PKG-INFO +13 -3
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/README.md +12 -2
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/docs/api/client.md +3 -1
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/docs/api/events.md +29 -2
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/docs/api/exceptions.md +6 -4
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/docs/api/models.md +5 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/docs/api/symbols.md +28 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/docs/getting-started.md +8 -1
- ctrader_api_client-0.12.0/justfile +88 -0
- ctrader_api_client-0.12.0/plans/nested-event-models.md +187 -0
- ctrader_api_client-0.12.0/plans/symbol-swaps.md +209 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/pyproject.toml +1 -1
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/__init__.py +6 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/api/trading.py +1 -1
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/auth/_maintain.py +5 -40
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/auth/manager.py +66 -12
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/client.py +3 -2
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/composition.py +1 -2
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/enums.py +28 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/events/_execution.py +11 -34
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/events/router.py +4 -6
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/events/types.py +15 -22
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/exceptions.py +2 -2
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/models/__init__.py +2 -0
- ctrader_api_client-0.12.0/src/ctrader_api_client/models/swap.py +109 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/models/symbol.py +5 -7
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/api/test_trading.py +21 -13
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/auth/conftest.py +16 -8
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/auth/test_authentication.py +41 -2
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/auth/test_token_lifecycle.py +34 -14
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/events/test_routing.py +97 -34
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/unit/test_enum_mapping.py +10 -0
- ctrader_api_client-0.12.0/tests/unit/test_swap_conversion.py +155 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/unit/test_symbol_conversion.py +0 -6
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/uv.lock +1 -1
- ctrader_api_client-0.10.0/Justfile +0 -56
- ctrader_api_client-0.10.0/plans/connection-auth-refactor.md +0 -99
- ctrader_api_client-0.10.0/plans/connection-research.md +0 -43
- ctrader_api_client-0.10.0/plans/strict-enum-mapping.md +0 -65
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/.claude/settings.local.json +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/.complexipy_cache/.gitignore +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/.complexipy_cache/CACHEDIR.TAG +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/.complexipy_cache/README.md +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/.complexipy_cache/v/cache/functions +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/.github/workflows/docs.yml +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/.gitignore +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/.pre-commit-config.yaml +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/.python-version +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/LICENSE +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/docs/api/accounts.md +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/docs/api/enums.md +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/docs/api/market-data.md +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/docs/api/trading.md +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/docs/index.md +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/mkdocs.yml +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/protos/SOURCE +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/protos/VERSION +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/protos/update.sh +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/protos/vendor/OpenApiMessages.proto +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/protos/vendor/OpenApiModelMessages.proto +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/scripts/fix_proto_imports.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/_internal/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/_internal/backoff.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/_internal/clock.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/_internal/conversions.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/_internal/messages.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/api/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/api/_base.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/api/accounts.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/api/market_data.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/api/symbols.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/auth/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/auth/_session.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/auth/credentials.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/auth/policy.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/auth/store.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/config.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/connection/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/connection/heartbeat.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/connection/protocol.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/connection/supervisor.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/connection/transport.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/events/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/events/emitter.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/events/publisher.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/models/_base.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/models/account.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/models/commission.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/models/deal.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/models/market_data.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/models/order.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/models/position.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/models/requests.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/models/trading_hours.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/src/ctrader_api_client/py.typed +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/api/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/api/conftest.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/api/test_market_data.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/api/test_reference_data.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/api/test_subscription_restore.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/auth/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/connection/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/connection/conftest.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/connection/test_event_dispatch.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/connection/test_framing.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/connection/test_heartbeat.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/connection/test_reconnection.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/connection/test_request_response.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/events/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/events/conftest.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/events/test_subscriptions.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/behavior/test_client.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/conftest.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/harness/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/harness/clock.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/harness/factories.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/harness/recorder.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/harness/server.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/harness/signals.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/harness/stub_protocol.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/harness/wire.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/unit/__init__.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/unit/test_api_errors.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/unit/test_commission_conversion.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/unit/test_market_data_conversion.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/unit/test_request_conversion.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/unit/test_trading_hours.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/unit/test_trading_records.py +0 -0
- {ctrader_api_client-0.10.0 → ctrader_api_client-0.12.0}/tests/unit/test_wire_format.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ctrader-api-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.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
|
|
@@ -133,6 +133,9 @@ await client.auth.authenticate_trader(
|
|
|
133
133
|
# Tokens are automatically refreshed before expiry. A refresh that fails is
|
|
134
134
|
# retried on the next check rather than dropping the session, and surfaces as a
|
|
135
135
|
# TokenRefreshFailedEvent so a persistently dead refresh token is observable.
|
|
136
|
+
# An access token that has already expired is refreshed before the account is
|
|
137
|
+
# authenticated, so a process that starts after the expiry needs no new token.
|
|
138
|
+
# Only a refresh token the server refuses raises TokenExpiredError.
|
|
136
139
|
```
|
|
137
140
|
|
|
138
141
|
### Persisting Rotated Tokens
|
|
@@ -249,6 +252,7 @@ await client.trading.close_position(account_id, close_position)
|
|
|
249
252
|
```python
|
|
250
253
|
from ctrader_api_client import (
|
|
251
254
|
ExecutionEvent,
|
|
255
|
+
ExecutionType,
|
|
252
256
|
ReadyEvent,
|
|
253
257
|
ReconnectedEvent,
|
|
254
258
|
SpotEvent,
|
|
@@ -261,10 +265,16 @@ from ctrader_api_client import (
|
|
|
261
265
|
async def on_spot(event: SpotEvent):
|
|
262
266
|
print(f"{event.bid}/{event.ask}")
|
|
263
267
|
|
|
264
|
-
# Order executions
|
|
268
|
+
# Order executions. The event carries the Order, Position and Deal models
|
|
269
|
+
# that the trading API returns. Each one is None when the server sent none.
|
|
265
270
|
@client.on(ExecutionEvent, account_id=account_id)
|
|
266
271
|
async def on_execution(event: ExecutionEvent):
|
|
267
|
-
|
|
272
|
+
if event.order is None:
|
|
273
|
+
return # A swap or a deposit has no order.
|
|
274
|
+
print(f"Order {event.order.order_id}: {event.execution_type}")
|
|
275
|
+
if event.execution_type is ExecutionType.ORDER_FILLED and event.order.is_closing_order:
|
|
276
|
+
# The order type says why the position closed.
|
|
277
|
+
print(f"Closed by {event.order.order_type.name}, stop-out={event.order.is_stop_out}")
|
|
268
278
|
|
|
269
279
|
# Account ready (fires on initial auth, after reconnection, and after account-disconnect recovery)
|
|
270
280
|
# Subscriptions are already restored by this point; use it to reconcile your own state.
|
|
@@ -114,6 +114,9 @@ await client.auth.authenticate_trader(
|
|
|
114
114
|
# Tokens are automatically refreshed before expiry. A refresh that fails is
|
|
115
115
|
# retried on the next check rather than dropping the session, and surfaces as a
|
|
116
116
|
# TokenRefreshFailedEvent so a persistently dead refresh token is observable.
|
|
117
|
+
# An access token that has already expired is refreshed before the account is
|
|
118
|
+
# authenticated, so a process that starts after the expiry needs no new token.
|
|
119
|
+
# Only a refresh token the server refuses raises TokenExpiredError.
|
|
117
120
|
```
|
|
118
121
|
|
|
119
122
|
### Persisting Rotated Tokens
|
|
@@ -230,6 +233,7 @@ await client.trading.close_position(account_id, close_position)
|
|
|
230
233
|
```python
|
|
231
234
|
from ctrader_api_client import (
|
|
232
235
|
ExecutionEvent,
|
|
236
|
+
ExecutionType,
|
|
233
237
|
ReadyEvent,
|
|
234
238
|
ReconnectedEvent,
|
|
235
239
|
SpotEvent,
|
|
@@ -242,10 +246,16 @@ from ctrader_api_client import (
|
|
|
242
246
|
async def on_spot(event: SpotEvent):
|
|
243
247
|
print(f"{event.bid}/{event.ask}")
|
|
244
248
|
|
|
245
|
-
# Order executions
|
|
249
|
+
# Order executions. The event carries the Order, Position and Deal models
|
|
250
|
+
# that the trading API returns. Each one is None when the server sent none.
|
|
246
251
|
@client.on(ExecutionEvent, account_id=account_id)
|
|
247
252
|
async def on_execution(event: ExecutionEvent):
|
|
248
|
-
|
|
253
|
+
if event.order is None:
|
|
254
|
+
return # A swap or a deposit has no order.
|
|
255
|
+
print(f"Order {event.order.order_id}: {event.execution_type}")
|
|
256
|
+
if event.execution_type is ExecutionType.ORDER_FILLED and event.order.is_closing_order:
|
|
257
|
+
# The order type says why the position closed.
|
|
258
|
+
print(f"Closed by {event.order.order_type.name}, stop-out={event.order.is_stop_out}")
|
|
249
259
|
|
|
250
260
|
# Account ready (fires on initial auth, after reconnection, and after account-disconnect recovery)
|
|
251
261
|
# Subscriptions are already restored by this point; use it to reconcile your own state.
|
|
@@ -70,7 +70,9 @@ Token-refresh and session-recovery timing, passed via
|
|
|
70
70
|
|
|
71
71
|
The `client.auth` property provides access to authentication operations. The
|
|
72
72
|
application is authenticated as the client connects, so what is left here is
|
|
73
|
-
authenticating trading accounts and asking after the sessions they hold.
|
|
73
|
+
authenticating trading accounts and asking after the sessions they hold. An
|
|
74
|
+
account whose access token has expired is refreshed before it is authenticated,
|
|
75
|
+
so the refresh token in the credentials is what a restart depends on.
|
|
74
76
|
|
|
75
77
|
::: ctrader_api_client.auth.AuthManager
|
|
76
78
|
options:
|
|
@@ -14,7 +14,7 @@ async def on_spot(event: SpotEvent):
|
|
|
14
14
|
|
|
15
15
|
@client.on(ExecutionEvent, account_id=12345) # Filter by account
|
|
16
16
|
async def on_execution(event: ExecutionEvent):
|
|
17
|
-
print(f"Order {event.order_id}: {event.execution_type}")
|
|
17
|
+
print(f"Order {event.order.order_id}: {event.execution_type}")
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
### Filtering
|
|
@@ -24,7 +24,7 @@ Different events support different filters:
|
|
|
24
24
|
| Event | `account_id` | `symbol_id` |
|
|
25
25
|
|-------|--------------|-------------|
|
|
26
26
|
| SpotEvent | Yes | Yes |
|
|
27
|
-
| ExecutionEvent | Yes |
|
|
27
|
+
| ExecutionEvent | Yes | No |
|
|
28
28
|
| DepthEvent | Yes | Yes |
|
|
29
29
|
| ReadyEvent | Yes | No |
|
|
30
30
|
| OrderErrorEvent | Yes | No |
|
|
@@ -72,6 +72,30 @@ async def on_spot(event: SpotEvent):
|
|
|
72
72
|
|
|
73
73
|
## Trading Events
|
|
74
74
|
|
|
75
|
+
An execution event carries the `Order`, `Position` and `Deal` models that the
|
|
76
|
+
trading API returns. Each one is `None` when the server sent no such message.
|
|
77
|
+
A swap or a deposit has no order. An accepted order has no position and no deal
|
|
78
|
+
yet.
|
|
79
|
+
|
|
80
|
+
The order says why a position closed:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from ctrader_api_client import ExecutionEvent, ExecutionType, OrderType
|
|
84
|
+
|
|
85
|
+
@client.on(ExecutionEvent, account_id=12345)
|
|
86
|
+
async def on_execution(event: ExecutionEvent):
|
|
87
|
+
if event.execution_type is not ExecutionType.ORDER_FILLED or event.order is None:
|
|
88
|
+
return
|
|
89
|
+
if not event.order.is_closing_order:
|
|
90
|
+
return # An entry fill.
|
|
91
|
+
if event.order.is_stop_out:
|
|
92
|
+
print("Closed by a margin stop-out")
|
|
93
|
+
elif event.order.order_type is OrderType.STOP_LOSS_TAKE_PROFIT:
|
|
94
|
+
print("Closed by a stop loss or a take profit")
|
|
95
|
+
else:
|
|
96
|
+
print("Closed by a market order")
|
|
97
|
+
```
|
|
98
|
+
|
|
75
99
|
::: ctrader_api_client.events.ExecutionEvent
|
|
76
100
|
options:
|
|
77
101
|
show_source: false
|
|
@@ -113,6 +137,9 @@ suppressed for `TOKEN_REFRESH` (session intact).
|
|
|
113
137
|
show_source: false
|
|
114
138
|
members: true
|
|
115
139
|
|
|
140
|
+
A trader update carries the `Account` model that the accounts API returns.
|
|
141
|
+
Its balance is a `Decimal` in account currency units.
|
|
142
|
+
|
|
116
143
|
::: ctrader_api_client.events.TraderUpdateEvent
|
|
117
144
|
options:
|
|
118
145
|
show_source: false
|
|
@@ -19,7 +19,7 @@ CTraderError
|
|
|
19
19
|
├── AuthenticationError
|
|
20
20
|
│ ├── ApplicationAuthError the server rejected client_id/secret
|
|
21
21
|
│ ├── AccountAuthError the server rejected the account
|
|
22
|
-
│ ├── TokenExpiredError the
|
|
22
|
+
│ ├── TokenExpiredError the server refused the access or refresh token
|
|
23
23
|
│ ├── TokenRefreshError a refresh exhausted its retries
|
|
24
24
|
│ └── AccountNotFoundError no account matches that trader login
|
|
25
25
|
├── APIError an error response to a request
|
|
@@ -69,9 +69,11 @@ except AccountAuthError as e:
|
|
|
69
69
|
print(f"Account {e.ctid_trader_account_id} rejected: {e.error_code}")
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
`TokenExpiredError`
|
|
73
|
-
|
|
74
|
-
token.
|
|
72
|
+
`TokenExpiredError` means the server refused a token: the access token at
|
|
73
|
+
authentication, or the refresh token when the client refreshed an expired access
|
|
74
|
+
token first. Neither can be retried back to life. It needs a new OAuth
|
|
75
|
+
authorization. An access token that has merely expired does not raise, because
|
|
76
|
+
the client refreshes it before it authenticates the account.
|
|
75
77
|
|
|
76
78
|
### Requests
|
|
77
79
|
|
|
@@ -190,6 +190,11 @@ symbol's quote currency, or in the currency that `minimum_asset` names. A `minim
|
|
|
190
190
|
means the broker sets no minimum.
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
::: ctrader_api_client.models.Swap
|
|
194
|
+
options:
|
|
195
|
+
show_source: false
|
|
196
|
+
|
|
197
|
+
|
|
193
198
|
::: ctrader_api_client.models.TradingHours
|
|
194
199
|
options:
|
|
195
200
|
show_source: false
|
|
@@ -87,6 +87,34 @@ charge lands in USD for the first two types, and in the symbol's quote currency
|
|
|
87
87
|
[Models](models.md#ctrader_api_client.models.Commission) for the arithmetic of each type,
|
|
88
88
|
and for how to apply `minimum`.
|
|
89
89
|
|
|
90
|
+
## Swap
|
|
91
|
+
|
|
92
|
+
A broker states a swap as two rates and a type. The type says what the rates count, so
|
|
93
|
+
you must read them together. The same rate of `-1.5` is pips per lot for one symbol, and
|
|
94
|
+
an annual percent for another. A negative rate is a charge.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from ctrader_api_client import SwapCalculationType
|
|
98
|
+
|
|
99
|
+
symbol = await client.symbols.get_by_id(account_id, 270)
|
|
100
|
+
swap = symbol.swap
|
|
101
|
+
|
|
102
|
+
print(f"{swap.long} / {swap.short} ({swap.calculation_type.value})")
|
|
103
|
+
print(f"Tripled on {swap.triple_day.value}")
|
|
104
|
+
print(f"Charged every {swap.period_hours} hours from {swap.charge_time} UTC")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| Type | Meaning of the rates |
|
|
108
|
+
|------|----------------------|
|
|
109
|
+
| `PIPS` | Pips per lot, per charge period. |
|
|
110
|
+
| `PERCENTAGE` | Annual percent of the position value. |
|
|
111
|
+
| `POINTS` | Points per lot, per charge period. |
|
|
112
|
+
|
|
113
|
+
The broker charges the swap once per `period_hours`, starting at `charge_time` UTC. On
|
|
114
|
+
`triple_day` the charge is three times the rate, to cover the weekend. `period_hours` is
|
|
115
|
+
`None` when the broker does not publish it. See
|
|
116
|
+
[Models](models.md#ctrader_api_client.models.Swap) for every field.
|
|
117
|
+
|
|
90
118
|
## Trading Hours
|
|
91
119
|
|
|
92
120
|
Every `Symbol` arrives with its trading calendar attached, so asking when a symbol is open
|
|
@@ -146,7 +146,8 @@ async def on_price(event: SpotEvent):
|
|
|
146
146
|
@client.on(ExecutionEvent)
|
|
147
147
|
async def on_execution(event: ExecutionEvent):
|
|
148
148
|
"""Called when orders are executed."""
|
|
149
|
-
|
|
149
|
+
if event.order is not None:
|
|
150
|
+
print(f"Execution: {event.execution_type} for order {event.order.order_id}")
|
|
150
151
|
|
|
151
152
|
|
|
152
153
|
async def main():
|
|
@@ -235,6 +236,12 @@ The contract is write-only. Reading back at startup is yours, since only you kno
|
|
|
235
236
|
which accounts a given process is responsible for. See
|
|
236
237
|
[TokenStore](api/client.md#tokenstore) for the full contract.
|
|
237
238
|
|
|
239
|
+
A stored pair whose access token has expired still works. `authenticate_trader`
|
|
240
|
+
refreshes it first and saves the new pair through the store, so a process that
|
|
241
|
+
starts after the expiry needs no new token from the outside. Only a refresh token
|
|
242
|
+
that the server refuses raises `TokenExpiredError`, and that needs a new OAuth
|
|
243
|
+
authorization.
|
|
244
|
+
|
|
238
245
|
## Handling Errors
|
|
239
246
|
|
|
240
247
|
Everything the library raises derives from `CTraderError`. Bringing the client up
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# cTrader API client justfile
|
|
2
|
+
set shell := ["bash", "-euo", "pipefail", "-c"]
|
|
3
|
+
set script-interpreter := ["bash", "-euo", "pipefail"]
|
|
4
|
+
set positional-arguments
|
|
5
|
+
|
|
6
|
+
# Variables
|
|
7
|
+
proto_path := "protos/vendor"
|
|
8
|
+
python_out_path := "src/ctrader_api_client/_internal/proto"
|
|
9
|
+
|
|
10
|
+
# Default recipe. List all available recipes.
|
|
11
|
+
[private]
|
|
12
|
+
default:
|
|
13
|
+
@just --list
|
|
14
|
+
|
|
15
|
+
# Verify that the required toolchains are present
|
|
16
|
+
[group('build')]
|
|
17
|
+
[script]
|
|
18
|
+
check-deps:
|
|
19
|
+
if ! command -v uv &> /dev/null; then
|
|
20
|
+
echo "uv not found. Install from https://docs.astral.sh/uv/getting-started/installation/" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
echo "==> All required toolchains are present"
|
|
24
|
+
|
|
25
|
+
# Install the pre-commit hooks, then build the project. Extra arguments go to `uv sync`.
|
|
26
|
+
[group('build')]
|
|
27
|
+
[script]
|
|
28
|
+
setup *args: check-deps
|
|
29
|
+
echo "==> Install the pre-commit hooks"
|
|
30
|
+
uv tool install prek
|
|
31
|
+
uv tool run prek install --hook-type pre-commit
|
|
32
|
+
echo "==> Build the project"
|
|
33
|
+
just build "$@"
|
|
34
|
+
|
|
35
|
+
# Sync the Python dependencies. Extra arguments go to `uv sync`.
|
|
36
|
+
[group('build')]
|
|
37
|
+
build *args:
|
|
38
|
+
uv sync "$@"
|
|
39
|
+
|
|
40
|
+
# Format the code. Extra arguments go to `ruff format`.
|
|
41
|
+
[group('dev')]
|
|
42
|
+
fmt *args:
|
|
43
|
+
uv run ruff format "$@"
|
|
44
|
+
|
|
45
|
+
# Lint the code. Extra arguments go to `ruff check`.
|
|
46
|
+
[group('dev')]
|
|
47
|
+
lint *args:
|
|
48
|
+
uv run ruff check --fix "$@"
|
|
49
|
+
|
|
50
|
+
# Type-check the code. Extra arguments go to `zuban check`.
|
|
51
|
+
[group('dev')]
|
|
52
|
+
type-check *args:
|
|
53
|
+
uv run zuban check "$@"
|
|
54
|
+
|
|
55
|
+
# Run the tests. Extra arguments go to pytest.
|
|
56
|
+
[group('dev')]
|
|
57
|
+
test *args:
|
|
58
|
+
uv run pytest "$@"
|
|
59
|
+
|
|
60
|
+
# Run the whole CI pipeline
|
|
61
|
+
[group('dev')]
|
|
62
|
+
@ci:
|
|
63
|
+
just fmt
|
|
64
|
+
just lint
|
|
65
|
+
just type-check
|
|
66
|
+
just test
|
|
67
|
+
|
|
68
|
+
# Serve the documentation locally. Extra arguments go to `mkdocs serve`.
|
|
69
|
+
[group('tools')]
|
|
70
|
+
documentation *args:
|
|
71
|
+
uv run mkdocs serve "$@"
|
|
72
|
+
|
|
73
|
+
# Fetch the .proto files at the given version into the vendor directory
|
|
74
|
+
[group('tools')]
|
|
75
|
+
update-proto version='main':
|
|
76
|
+
./protos/update.sh "$1"
|
|
77
|
+
|
|
78
|
+
# Generate the Python code from the .proto files. Run this after `update-proto`.
|
|
79
|
+
[group('tools')]
|
|
80
|
+
[script]
|
|
81
|
+
compile-proto:
|
|
82
|
+
echo "==> Compile the .proto files from {{ proto_path }}/ into {{ python_out_path }}/"
|
|
83
|
+
uv run protoc -I={{ proto_path }} --python_betterproto_out={{ python_out_path }} {{ proto_path }}/*.proto
|
|
84
|
+
echo "==> Fix the cross-module imports"
|
|
85
|
+
uv run python scripts/fix_proto_imports.py
|
|
86
|
+
echo "==> Format the generated code"
|
|
87
|
+
uv run ruff check --fix {{ python_out_path }}
|
|
88
|
+
uv run ruff format {{ python_out_path }}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Nested event models
|
|
2
|
+
|
|
3
|
+
Status: steps 1 to 3 done on 2026-10-21. The version bump to 0.12.0 waits for another change that ships with it.
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
A consumer cannot tell why a position closed from the live event stream.
|
|
8
|
+
|
|
9
|
+
- A close arrives as a `ProtoOAExecutionEvent` of type `ORDER_FILLED`. Its
|
|
10
|
+
embedded `ProtoOAOrder` says why: `order_type` is `STOP_LOSS_TAKE_PROFIT` for
|
|
11
|
+
a protection close and `MARKET` for a manual close, `closing_order` marks a
|
|
12
|
+
close, and `is_stop_out` marks a margin close.
|
|
13
|
+
- `execution_event_from_proto` keeps only the side, the order id and the symbol
|
|
14
|
+
id from the order. It keeps only the filled volume, the price and the
|
|
15
|
+
timestamp from the deal. It keeps only the position id from the position.
|
|
16
|
+
- The only public path to the order type is `TradingAPI.get_orders`, which is a
|
|
17
|
+
second round trip that races the next event.
|
|
18
|
+
|
|
19
|
+
The same shape repeats in `TraderUpdateEvent`. The server sends a full
|
|
20
|
+
`ProtoOATrader`, and the router keeps three of its thirty-two fields.
|
|
21
|
+
|
|
22
|
+
The library already has a faithful model for each embedded message: `Order`,
|
|
23
|
+
`Position`, `Deal` and `Account`. The events translate the same data a second
|
|
24
|
+
time, and translate less of it.
|
|
25
|
+
|
|
26
|
+
## Facts from the source
|
|
27
|
+
|
|
28
|
+
- `events/_execution.py`: `execution_event_from_proto` reads presence of the
|
|
29
|
+
order, the position and the deal with `betterproto.serialized_on_wire`. It
|
|
30
|
+
falls back to `datetime.now(UTC)` for the timestamp when the event has no
|
|
31
|
+
deal. Both `TradingAPI` and `EventRouter` call it.
|
|
32
|
+
- `models/order.py`, `models/position.py`: `from_proto` tolerates an absent
|
|
33
|
+
`trade_data`. Both require a known `order_type`, `order_status` or
|
|
34
|
+
`position_status`; an unset enum raises `UnknownEnumValueError`.
|
|
35
|
+
- `models/deal.py`: `from_proto` requires a known `trade_side` and
|
|
36
|
+
`deal_status`. It reads the close detail only when its balance is positive.
|
|
37
|
+
- `models/account.py`: `Account.from_proto` scales `balance` by
|
|
38
|
+
`money_divisor(money_digits)`, which reads 0 as cents. `TraderUpdateEvent`
|
|
39
|
+
carries the raw integer plus `money_digits` instead, so a live balance and a
|
|
40
|
+
fetched balance are in different units.
|
|
41
|
+
- `events/router.py`: `_handle_trader_update` drops the event when the trader
|
|
42
|
+
is absent. `_handle_execution` logs `event.order_id`.
|
|
43
|
+
- `events/emitter.py`: `_validate_filter` checks `__dataclass_fields__` for
|
|
44
|
+
`account_id` and `symbol_id`. `_matches_filter` reads them with `getattr`.
|
|
45
|
+
- `client.py`: `T_BothFilters` names `SpotEvent`, `ExecutionEvent` and
|
|
46
|
+
`DepthEvent` as the events that accept a `symbol_id` filter. No test pins the
|
|
47
|
+
symbol filter on `ExecutionEvent`.
|
|
48
|
+
- Tests that construct or read the two events: `tests/behavior/events/test_routing.py`,
|
|
49
|
+
`tests/behavior/api/test_trading.py`, `tests/unit/test_enum_mapping.py`. The
|
|
50
|
+
routing tests build orders without an order type or status, and deals without
|
|
51
|
+
a side or status.
|
|
52
|
+
- Docs that show `event.order_id`: `README.md`, `docs/getting-started.md`,
|
|
53
|
+
`docs/api/events.md`, and the docstrings of `Client.on` and `TradingAPI`.
|
|
54
|
+
|
|
55
|
+
## Design
|
|
56
|
+
|
|
57
|
+
1. `ExecutionEvent` becomes:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
account_id: int
|
|
61
|
+
execution_type: ExecutionType
|
|
62
|
+
order: Order | None
|
|
63
|
+
position: Position | None
|
|
64
|
+
deal: Deal | None
|
|
65
|
+
is_server_event: bool = False
|
|
66
|
+
error_code: str | None = None
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Each nested field is `None` when the sub-message was not on the wire. The
|
|
70
|
+
converter calls the three existing `from_proto` methods. `order_id`,
|
|
71
|
+
`position_id`, `symbol_id`, `side`, `filled_volume`, `fill_price` and
|
|
72
|
+
`timestamp` leave. The execution timestamp lives on `deal.execution_timestamp`,
|
|
73
|
+
and an event with no deal has no timestamp instead of a fabricated one.
|
|
74
|
+
|
|
75
|
+
2. `TraderUpdateEvent` becomes:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
account_id: int
|
|
79
|
+
account: Account
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The router already drops an event with no trader, so `account` is not
|
|
83
|
+
optional. `balance`, `leverage_in_cents` and `money_digits` leave. The
|
|
84
|
+
balance is a scaled `Decimal` from the one converter that owns the scaling.
|
|
85
|
+
|
|
86
|
+
3. `ExecutionEvent` loses the `symbol_id` filter. The symbol lives on the nested
|
|
87
|
+
order, and a swap or a deposit event has no order at all. `T_BothFilters`
|
|
88
|
+
drops `ExecutionEvent`, and the filter table in `docs/api/events.md` says
|
|
89
|
+
`No`. A consumer that wants one symbol reads `event.order.symbol_id` in the
|
|
90
|
+
handler.
|
|
91
|
+
|
|
92
|
+
4. The reason for a close reads from one event:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
if event.execution_type is ExecutionType.ORDER_FILLED and event.order.is_closing_order:
|
|
96
|
+
... # event.order.order_type, event.order.is_stop_out
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### What does not change
|
|
100
|
+
|
|
101
|
+
- An execution type the client does not know still raises from the converter.
|
|
102
|
+
The API layer lets it reach the caller, and the router drops the event. A
|
|
103
|
+
nested message with an unknown enum now takes the same path, because the
|
|
104
|
+
model converters raise the same error.
|
|
105
|
+
- `account_id` stays on both events. It is the routing key that the account
|
|
106
|
+
filter reads, and every event in `events/types.py` carries it.
|
|
107
|
+
- `MarginCallTriggerEvent` keeps its flat shape. There is no `MarginCall` model,
|
|
108
|
+
and no consumer asks for the fields the event drops.
|
|
109
|
+
- `DepthEvent` keeps `DepthQuote` in `events/types.py`. No other message uses it.
|
|
110
|
+
- `bonus_deposit_withdraw` and `deposit_withdraw` stay unread. There is no
|
|
111
|
+
model for them and no consumer asks for them.
|
|
112
|
+
- The `Order`, `Position`, `Deal` and `Account` models do not change.
|
|
113
|
+
|
|
114
|
+
### Attribute counts
|
|
115
|
+
|
|
116
|
+
- `ExecutionEvent`: 10 today, 7 after.
|
|
117
|
+
- `TraderUpdateEvent`: 4 today, 2 after.
|
|
118
|
+
|
|
119
|
+
## Steps
|
|
120
|
+
|
|
121
|
+
One step per turn. Each step leaves the suite green.
|
|
122
|
+
|
|
123
|
+
### Step 1: `ExecutionEvent` nests `Order`, `Position` and `Deal`
|
|
124
|
+
|
|
125
|
+
- `events/types.py`: the new field list. The docstring states that each nested
|
|
126
|
+
field is `None` when the server sent no such message.
|
|
127
|
+
- `events/_execution.py`: call `Order.from_proto`, `Position.from_proto` and
|
|
128
|
+
`Deal.from_proto` behind the existing presence checks. Remove the timestamp
|
|
129
|
+
fallback and the `Decimal` and `order_side` imports.
|
|
130
|
+
- `events/router.py`: the debug line reads the order id through
|
|
131
|
+
`event.order`, and logs `None` when there is no order.
|
|
132
|
+
- `client.py`: `T_BothFilters` drops `ExecutionEvent`. The `Client.on`
|
|
133
|
+
docstring example reads `event.order.order_id`.
|
|
134
|
+
- `api/trading.py`: the class docstring example reads `execution.order.order_id`.
|
|
135
|
+
- `docs/api/events.md`: the filter table marks `ExecutionEvent` as `No` for
|
|
136
|
+
`symbol_id`. The code example reads `event.order.order_id`.
|
|
137
|
+
- `tests/behavior/events/test_routing.py`: the `order()` helper carries an
|
|
138
|
+
order type and a status. Deals carry a side and a status. Positions carry a
|
|
139
|
+
status. The assertions read through `event.order`, `event.position` and
|
|
140
|
+
`event.deal`. Replace the four tests on the flat fields with:
|
|
141
|
+
1. A fill carries its order, position and deal: `event.order.order_id`,
|
|
142
|
+
`event.position.position_id`, `event.deal.filled_volume`,
|
|
143
|
+
`event.deal.execution_price` and `event.deal.execution_timestamp`.
|
|
144
|
+
2. A sell execution reports the sell side through `event.order.side`.
|
|
145
|
+
3. An accepted order has no position and no deal: both are `None`.
|
|
146
|
+
4. A swap has no order: `event.order is None`.
|
|
147
|
+
5. A close reports its reason: an `ORDER_FILLED` event whose order has
|
|
148
|
+
`order_type=STOP_LOSS_TAKE_PROFIT` and `closing_order=True` yields
|
|
149
|
+
`event.order.is_closing_order` and `event.order.order_type is
|
|
150
|
+
OrderType.STOP_LOSS_TAKE_PROFIT`.
|
|
151
|
+
- `tests/behavior/api/test_trading.py`: the two tests that read the flat
|
|
152
|
+
fields of a placed order read through the nested models instead.
|
|
153
|
+
- `tests/unit/test_enum_mapping.py`: `test_an_unknown_execution_type_is_an_error`
|
|
154
|
+
passes unchanged.
|
|
155
|
+
|
|
156
|
+
### Step 2: `TraderUpdateEvent` nests `Account`
|
|
157
|
+
|
|
158
|
+
- `events/types.py`: the new field list.
|
|
159
|
+
- `events/router.py`: `_handle_trader_update` builds `Account.from_proto(trader)`.
|
|
160
|
+
Remove the `DEFAULT_MONEY_DIGITS` use from this handler only; the margin
|
|
161
|
+
handler keeps it.
|
|
162
|
+
- `tests/behavior/events/test_routing.py`: replace the two trader update tests
|
|
163
|
+
with:
|
|
164
|
+
1. A trader update carries the account: `event.account.balance ==
|
|
165
|
+
Decimal("10000.00")` for a wire balance of `1_000_000` at two money
|
|
166
|
+
digits, and `event.account.leverage_in_cents == 10_000`.
|
|
167
|
+
2. A trader update without money digits scales as cents: a wire balance of
|
|
168
|
+
`1_000_000` with no `money_digits` yields `Decimal("10000.00")`.
|
|
169
|
+
|
|
170
|
+
### Step 3: docs and version
|
|
171
|
+
|
|
172
|
+
- `README.md` and `docs/getting-started.md`: the execution handler example
|
|
173
|
+
reads `event.order.order_id`. Add a short example that reads the reason for a
|
|
174
|
+
close from `event.order`.
|
|
175
|
+
- `docs/api/events.md`: the `ExecutionEvent` section states that the three
|
|
176
|
+
nested models are the same types that `TradingAPI` returns.
|
|
177
|
+
- `pyproject.toml`: 0.12.0. Deferred: another change ships in the same bump.
|
|
178
|
+
|
|
179
|
+
## Consumer follow-up
|
|
180
|
+
|
|
181
|
+
Out of scope for this repository. Recorded so the two changes stay aligned.
|
|
182
|
+
|
|
183
|
+
Each consumer bumps its pin to 0.12.0 and reads `event.order`, `event.position`
|
|
184
|
+
and `event.deal` instead of the flat fields. A handler on `ExecutionEvent` that
|
|
185
|
+
filtered by `symbol_id` moves the check into the handler body. A handler on
|
|
186
|
+
`TraderUpdateEvent` reads `event.account.balance` as a `Decimal` in account
|
|
187
|
+
currency units and drops its own scaling.
|