ctrader-api-client 0.5.1__tar.gz → 0.7.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.7.0/.claude/settings.local.json +8 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/.gitignore +4 -1
- ctrader_api_client-0.7.0/HARDENING.md +251 -0
- ctrader_api_client-0.7.0/PKG-INFO +377 -0
- ctrader_api_client-0.7.0/README.md +358 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/docs/api/accounts.md +8 -4
- ctrader_api_client-0.7.0/docs/api/client.md +133 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/docs/api/enums.md +1 -7
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/docs/api/events.md +36 -12
- ctrader_api_client-0.7.0/docs/api/exceptions.md +213 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/docs/api/market-data.md +26 -15
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/docs/api/models.md +2 -3
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/docs/api/trading.md +3 -4
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/docs/getting-started.md +132 -35
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/docs/index.md +14 -13
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/mkdocs.yml +1 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/pyproject.toml +27 -1
- ctrader_api_client-0.7.0/src/ctrader_api_client/__init__.py +190 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/_internal/__init__.py +11 -4
- ctrader_api_client-0.7.0/src/ctrader_api_client/_internal/clock.py +36 -0
- ctrader_api_client-0.7.0/src/ctrader_api_client/_internal/conversions.py +41 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/_internal/messages.py +13 -43
- ctrader_api_client-0.7.0/src/ctrader_api_client/api/_base.py +25 -0
- ctrader_api_client-0.7.0/src/ctrader_api_client/api/accounts.py +134 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/api/market_data.py +103 -70
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/api/symbols.py +8 -34
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/api/trading.py +57 -199
- ctrader_api_client-0.7.0/src/ctrader_api_client/auth/__init__.py +26 -0
- ctrader_api_client-0.7.0/src/ctrader_api_client/auth/_recovery.py +238 -0
- ctrader_api_client-0.7.0/src/ctrader_api_client/auth/_refresh.py +203 -0
- ctrader_api_client-0.7.0/src/ctrader_api_client/auth/_session.py +180 -0
- ctrader_api_client-0.7.0/src/ctrader_api_client/auth/manager.py +259 -0
- ctrader_api_client-0.7.0/src/ctrader_api_client/auth/policy.py +47 -0
- ctrader_api_client-0.7.0/src/ctrader_api_client/auth/store.py +35 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/client.py +145 -204
- ctrader_api_client-0.7.0/src/ctrader_api_client/composition.py +165 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/config.py +8 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/connection/__init__.py +4 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/connection/heartbeat.py +41 -31
- ctrader_api_client-0.7.0/src/ctrader_api_client/connection/listener.py +17 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/connection/protocol.py +180 -59
- ctrader_api_client-0.7.0/src/ctrader_api_client/connection/supervisor.py +124 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/enums.py +12 -5
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/events/__init__.py +6 -0
- ctrader_api_client-0.7.0/src/ctrader_api_client/events/_execution.py +92 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/events/emitter.py +42 -43
- ctrader_api_client-0.7.0/src/ctrader_api_client/events/publisher.py +19 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/events/router.py +54 -136
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/events/types.py +51 -4
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/exceptions.py +17 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/models/account.py +6 -11
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/models/deal.py +7 -13
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/models/order.py +23 -16
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/models/position.py +20 -19
- ctrader_api_client-0.7.0/tests/__init__.py +1 -0
- ctrader_api_client-0.7.0/tests/behavior/__init__.py +1 -0
- ctrader_api_client-0.7.0/tests/behavior/api/__init__.py +1 -0
- ctrader_api_client-0.7.0/tests/behavior/api/conftest.py +34 -0
- ctrader_api_client-0.7.0/tests/behavior/api/test_market_data.py +348 -0
- ctrader_api_client-0.7.0/tests/behavior/api/test_reference_data.py +295 -0
- ctrader_api_client-0.7.0/tests/behavior/api/test_subscription_restore.py +206 -0
- ctrader_api_client-0.7.0/tests/behavior/api/test_trading.py +492 -0
- ctrader_api_client-0.7.0/tests/behavior/auth/__init__.py +1 -0
- ctrader_api_client-0.7.0/tests/behavior/auth/conftest.py +120 -0
- ctrader_api_client-0.7.0/tests/behavior/auth/test_authentication.py +265 -0
- ctrader_api_client-0.7.0/tests/behavior/auth/test_token_lifecycle.py +416 -0
- ctrader_api_client-0.7.0/tests/behavior/connection/__init__.py +1 -0
- ctrader_api_client-0.7.0/tests/behavior/connection/conftest.py +24 -0
- ctrader_api_client-0.7.0/tests/behavior/connection/test_event_dispatch.py +141 -0
- ctrader_api_client-0.7.0/tests/behavior/connection/test_framing.py +122 -0
- ctrader_api_client-0.7.0/tests/behavior/connection/test_heartbeat.py +130 -0
- ctrader_api_client-0.7.0/tests/behavior/connection/test_reconnection.py +161 -0
- ctrader_api_client-0.7.0/tests/behavior/connection/test_request_response.py +107 -0
- ctrader_api_client-0.7.0/tests/behavior/events/__init__.py +1 -0
- ctrader_api_client-0.7.0/tests/behavior/events/conftest.py +32 -0
- ctrader_api_client-0.7.0/tests/behavior/events/test_routing.py +697 -0
- ctrader_api_client-0.7.0/tests/behavior/events/test_subscriptions.py +318 -0
- ctrader_api_client-0.7.0/tests/behavior/test_client.py +438 -0
- ctrader_api_client-0.7.0/tests/conftest.py +160 -0
- ctrader_api_client-0.7.0/tests/harness/__init__.py +44 -0
- ctrader_api_client-0.7.0/tests/harness/clock.py +99 -0
- ctrader_api_client-0.7.0/tests/harness/factories.py +130 -0
- ctrader_api_client-0.7.0/tests/harness/recorder.py +163 -0
- ctrader_api_client-0.7.0/tests/harness/server.py +268 -0
- ctrader_api_client-0.7.0/tests/harness/signals.py +58 -0
- ctrader_api_client-0.7.0/tests/harness/stub_protocol.py +192 -0
- ctrader_api_client-0.7.0/tests/harness/wire.py +94 -0
- ctrader_api_client-0.7.0/tests/unit/__init__.py +1 -0
- ctrader_api_client-0.7.0/tests/unit/test_api_errors.py +71 -0
- ctrader_api_client-0.7.0/tests/unit/test_market_data_conversion.py +127 -0
- ctrader_api_client-0.7.0/tests/unit/test_request_conversion.py +221 -0
- ctrader_api_client-0.7.0/tests/unit/test_symbol_conversion.py +144 -0
- ctrader_api_client-0.7.0/tests/unit/test_trading_records.py +302 -0
- ctrader_api_client-0.7.0/tests/unit/test_wire_format.py +199 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/uv.lock +100 -1
- ctrader_api_client-0.5.1/PKG-INFO +0 -259
- ctrader_api_client-0.5.1/README.md +0 -240
- ctrader_api_client-0.5.1/docs/api/client.md +0 -52
- ctrader_api_client-0.5.1/src/ctrader_api_client/__init__.py +0 -66
- ctrader_api_client-0.5.1/src/ctrader_api_client/api/accounts.py +0 -71
- ctrader_api_client-0.5.1/src/ctrader_api_client/auth/__init__.py +0 -16
- ctrader_api_client-0.5.1/src/ctrader_api_client/auth/manager.py +0 -617
- ctrader_api_client-0.5.1/src/ctrader_api_client/auth/trigger.py +0 -17
- ctrader_api_client-0.5.1/tests/unit/_internal/test_messages.py +0 -270
- ctrader_api_client-0.5.1/tests/unit/_internal/test_serialization.py +0 -172
- ctrader_api_client-0.5.1/tests/unit/api/conftest.py +0 -13
- ctrader_api_client-0.5.1/tests/unit/api/test_accounts.py +0 -116
- ctrader_api_client-0.5.1/tests/unit/api/test_market_data_api.py +0 -220
- ctrader_api_client-0.5.1/tests/unit/api/test_symbols.py +0 -281
- ctrader_api_client-0.5.1/tests/unit/api/test_trading.py +0 -434
- ctrader_api_client-0.5.1/tests/unit/auth/test_credentials.py +0 -177
- ctrader_api_client-0.5.1/tests/unit/auth/test_manager.py +0 -962
- ctrader_api_client-0.5.1/tests/unit/connection/test_heartbeat.py +0 -219
- ctrader_api_client-0.5.1/tests/unit/connection/test_protocol.py +0 -502
- ctrader_api_client-0.5.1/tests/unit/connection/test_transport.py +0 -244
- ctrader_api_client-0.5.1/tests/unit/events/test_emitter.py +0 -352
- ctrader_api_client-0.5.1/tests/unit/events/test_router.py +0 -581
- ctrader_api_client-0.5.1/tests/unit/events/test_types.py +0 -320
- ctrader_api_client-0.5.1/tests/unit/models/test_account.py +0 -204
- ctrader_api_client-0.5.1/tests/unit/models/test_deal.py +0 -244
- ctrader_api_client-0.5.1/tests/unit/models/test_market_data.py +0 -159
- ctrader_api_client-0.5.1/tests/unit/models/test_order.py +0 -310
- ctrader_api_client-0.5.1/tests/unit/models/test_position.py +0 -175
- ctrader_api_client-0.5.1/tests/unit/models/test_requests.py +0 -380
- ctrader_api_client-0.5.1/tests/unit/models/test_symbol.py +0 -236
- ctrader_api_client-0.5.1/tests/unit/test_client.py +0 -549
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/.github/workflows/docs.yml +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/.pre-commit-config.yaml +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/.python-version +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/Justfile +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/LICENSE +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/docs/api/symbols.md +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/protos/SOURCE +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/protos/VERSION +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/protos/update.sh +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/protos/vendor/OpenApiMessages.proto +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/protos/vendor/OpenApiModelMessages.proto +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/scripts/fix_proto_imports.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/api/__init__.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/auth/credentials.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/connection/transport.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/models/__init__.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/models/_base.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/models/market_data.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/models/requests.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/models/symbol.py +0 -0
- {ctrader_api_client-0.5.1 → ctrader_api_client-0.7.0}/src/ctrader_api_client/py.typed +0 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Architecture hardening
|
|
2
|
+
|
|
3
|
+
Working notes for the `harden-architecture` branch. Everything below is against
|
|
4
|
+
`master` at `8f649f2`.
|
|
5
|
+
|
|
6
|
+
## Done
|
|
7
|
+
|
|
8
|
+
Each item is one commit, in order.
|
|
9
|
+
|
|
10
|
+
### Connection and auth lifecycle tests — `de41c4d`
|
|
11
|
+
|
|
12
|
+
Edge cases the existing suite did not reach, added before any of the
|
|
13
|
+
refactoring below so the refactoring had something to fail against: framing,
|
|
14
|
+
reconnection, token lifecycle and client startup.
|
|
15
|
+
|
|
16
|
+
### Typed request correlation — `bb66230`
|
|
17
|
+
|
|
18
|
+
`Protocol` tracked in-flight requests across three parallel dicts keyed by
|
|
19
|
+
message id. They could disagree. Replaced by a single object holding the event,
|
|
20
|
+
the outcome and the expected response type, so a pending request is one thing
|
|
21
|
+
that either exists or does not.
|
|
22
|
+
|
|
23
|
+
### Event handler registration — `4131424`
|
|
24
|
+
|
|
25
|
+
Registering a handler and unregistering it were written out separately at every
|
|
26
|
+
call site. `on_event` now returns a disposer, and callers hold that instead of
|
|
27
|
+
remembering how to undo their own registration.
|
|
28
|
+
|
|
29
|
+
### Pluggable token storage — `b6dc7df`
|
|
30
|
+
|
|
31
|
+
Added the `TokenStore` protocol and the option to pass one to the client. The
|
|
32
|
+
client writes through it whenever tokens rotate, so a process that restarts can
|
|
33
|
+
pick up where it left off rather than re-authenticating from credentials.
|
|
34
|
+
|
|
35
|
+
Two things moved in passing, both breaking:
|
|
36
|
+
|
|
37
|
+
- `AuthTrigger` moved from `auth/trigger.py`, which is gone, to `enums.py`. It
|
|
38
|
+
describes why a session was established, which the event types need and the
|
|
39
|
+
auth package should not own alone.
|
|
40
|
+
- `ReadyEvent.is_reconnect` is now a property over a `trigger: AuthTrigger`
|
|
41
|
+
field. Reading it is unchanged; constructing a `ReadyEvent` is not.
|
|
42
|
+
|
|
43
|
+
### Dead code and a start() guard — `de21183`
|
|
44
|
+
|
|
45
|
+
Removed `on_handler_error()`, which nothing called. Made
|
|
46
|
+
`HeartbeatManager.start()` idempotent: a second call used to overwrite the
|
|
47
|
+
nursery reference and leave the first loop unreachable, so `stop()` could never
|
|
48
|
+
find it.
|
|
49
|
+
|
|
50
|
+
### Automatic subscription restore — `e0a3273`
|
|
51
|
+
|
|
52
|
+
Subscriptions were restored by an explicit call after reconnection, which meant
|
|
53
|
+
every reconnection path had to remember to make it. `MarketDataAPI` is now
|
|
54
|
+
registered as a restorer and the reconnect path drives it.
|
|
55
|
+
|
|
56
|
+
### TokenStore documentation — `2f17ac3`
|
|
57
|
+
|
|
58
|
+
No code change. `TokenStore` is write-only by design — the library never reads
|
|
59
|
+
it back, because only the caller knows which accounts a given process is
|
|
60
|
+
responsible for. The name suggested otherwise, so the docstrings, the README
|
|
61
|
+
example and `docs/api/client.md` now say so and show the startup branch the
|
|
62
|
+
caller is expected to write.
|
|
63
|
+
|
|
64
|
+
Deliberately **not** done: adding `load()` to the protocol. The library would
|
|
65
|
+
have no reason to call it.
|
|
66
|
+
|
|
67
|
+
### Composition root — `b3e6fb5`
|
|
68
|
+
|
|
69
|
+
`CTraderClient.__init__` built eleven collaborators and wired them together,
|
|
70
|
+
which made the client impossible to construct with a substitute for any one of
|
|
71
|
+
them. That wiring moved verbatim to `composition.py` as `build_graph()`
|
|
72
|
+
returning a frozen `ClientGraph`, and `CTraderClient.from_graph()` adopts one.
|
|
73
|
+
|
|
74
|
+
Two ordering constraints in the wiring are load-bearing and are noted in the
|
|
75
|
+
source: market data is built before auth because it is the restorer, and auth
|
|
76
|
+
before the router because it is the recovery.
|
|
77
|
+
|
|
78
|
+
Deliberately **not** done: a `build_client()` convenience wrapper.
|
|
79
|
+
`build_graph` plus `from_graph` already covers it.
|
|
80
|
+
|
|
81
|
+
### Lifecycle inversion — `bd92f44` — **breaking**
|
|
82
|
+
|
|
83
|
+
`Protocol`, `HeartbeatManager` and `AuthManager` each held a task group handed
|
|
84
|
+
to them by a `start()` call and exited by a later `stop()` call from a different
|
|
85
|
+
task. `start()` replaced by `serve()`, which owns its nursery lexically and
|
|
86
|
+
suspends inside it; `stop()` reduced to cancelling the scope, which is allowed
|
|
87
|
+
from any task.
|
|
88
|
+
|
|
89
|
+
`CTraderClient.connect()` and `.close()` are **gone**. `async with client:` is
|
|
90
|
+
the only way to bring a client up. The nursery lives in the `async with` block,
|
|
91
|
+
so a background loop that dies is raised out of it rather than discovered at
|
|
92
|
+
shutdown.
|
|
93
|
+
|
|
94
|
+
Three things fell out of this:
|
|
95
|
+
|
|
96
|
+
- Every `except Exception: pass` in a `stop()` is gone; there is nothing left
|
|
97
|
+
to swallow.
|
|
98
|
+
- Cleanup moved into each `serve()`'s `finally`, so cancellation now cleans up
|
|
99
|
+
as thoroughly as an orderly stop.
|
|
100
|
+
- `task_group.start` rather than `start_soon` in the client means the reader is
|
|
101
|
+
provably live before anything is sent. That ordering used to be implicit.
|
|
102
|
+
|
|
103
|
+
Test-side, `connected` and `serving` fixtures give the *test* ownership of the
|
|
104
|
+
task group, for the same reason: a loop that dies mid-test surfaces at the end
|
|
105
|
+
of it instead of vanishing.
|
|
106
|
+
|
|
107
|
+
### B1 — connection supervisor — `9df080b` — **breaking**
|
|
108
|
+
|
|
109
|
+
`Protocol` reported drops and recoveries through two callbacks the client set
|
|
110
|
+
on it by hand, and `CTraderClient._lifecycle` knew the order transport,
|
|
111
|
+
protocol, heartbeat and auth had to come up in. Both are gone.
|
|
112
|
+
|
|
113
|
+
`ConnectionSupervisor` owns the transport, the protocol and the heartbeat.
|
|
114
|
+
`serving()` is an async context manager rather than a `serve()`: connecting
|
|
115
|
+
happens before the block is entered, so a server that is not there still raises
|
|
116
|
+
`CTraderConnectionFailedError` instead of the `ExceptionGroup` a task group
|
|
117
|
+
would make of it. `_lifecycle` is now the supervisor's block with auth and the
|
|
118
|
+
router inside it.
|
|
119
|
+
|
|
120
|
+
The callbacks became one port. `ConnectionListener` — `on_connection_lost` and
|
|
121
|
+
`on_connection_restored` — is what `Protocol` reports to and what the
|
|
122
|
+
supervisor fans out to. The supervisor puts the heartbeat back itself before
|
|
123
|
+
notifying, because that is its own machinery rather than a listener's concern.
|
|
124
|
+
|
|
125
|
+
The listener is `AuthManager`, not the client. Re-authenticating the
|
|
126
|
+
application and every account after a reconnect, and reporting the outcome as
|
|
127
|
+
`ReconnectedEvent`, is session work; it only lived on the client because the
|
|
128
|
+
callback did. That takes ~70 lines off `client.py`, and puts the logic where
|
|
129
|
+
B3's `SessionRecovery` would want it.
|
|
130
|
+
|
|
131
|
+
Breaking: `Protocol.set_reconnect_handler` and `set_disconnect_handler` are
|
|
132
|
+
replaced by `Protocol.set_listener`, and `AuthManager.handle_connection_lost`
|
|
133
|
+
is now the private `_forget_sessions`, reached through the port.
|
|
134
|
+
|
|
135
|
+
Test-side, a `make_graph` fixture hands tests the graph before the client
|
|
136
|
+
adopts it, which is how the listener fan-out is exercised.
|
|
137
|
+
|
|
138
|
+
### Exception shape at the client boundary — `ff6137a`
|
|
139
|
+
|
|
140
|
+
Found while placing automatic application authentication. Since the lifecycle
|
|
141
|
+
inversion, the task group holding the background tasks surrounds the `yield` in
|
|
142
|
+
`_lifecycle`, so *every* exception leaving `async with client:` passed through
|
|
143
|
+
it — and anyio wraps a lone exception as readily as a group. A caller's own
|
|
144
|
+
`ValueError` came back as `ExceptionGroup: unhandled errors in a TaskGroup (1
|
|
145
|
+
sub-exception)`, and `except CTraderConnectionFailedError` would have stopped
|
|
146
|
+
matching anything the machinery raised.
|
|
147
|
+
|
|
148
|
+
`__aenter__` and `__aexit__` now peel single-exception groups, recursively,
|
|
149
|
+
since the supervisor's task group nests inside the client's. Several failures
|
|
150
|
+
still arrive as a group, because that is what they are, and the block's own
|
|
151
|
+
exception is returned to the caller untouched rather than re-raised.
|
|
152
|
+
|
|
153
|
+
Not a break against `master`: this restores the shape callers had before the
|
|
154
|
+
branch.
|
|
155
|
+
|
|
156
|
+
### B3 — split AuthManager — `651da82` — **breaking**
|
|
157
|
+
|
|
158
|
+
`auth/manager.py` was 751 lines doing four unrelated jobs. It is now four
|
|
159
|
+
pieces, each with one:
|
|
160
|
+
|
|
161
|
+
- `auth/_session.py` — `SessionStore`, joining the session state types already
|
|
162
|
+
there. It owns the dict, its invalidation and every query over it, so the
|
|
163
|
+
distinction between credentials (which outlive a link) and sessions (which do
|
|
164
|
+
not) is stated once instead of being re-derived by each collaborator.
|
|
165
|
+
- `auth/_refresh.py` — `TokenRefresher`, the refresh loop, its retry policy and
|
|
166
|
+
the `TokenStore` write-through.
|
|
167
|
+
- `auth/_recovery.py` — `SessionRecovery`, the reauth loop and the
|
|
168
|
+
`ConnectionListener` methods. It is also the `SessionRecovery` port the event
|
|
169
|
+
router already typed its collaborator as, so the names finally line up.
|
|
170
|
+
- `auth/manager.py` — `AuthManager`, down to turning credentials into live
|
|
171
|
+
sessions.
|
|
172
|
+
|
|
173
|
+
The refresher and the recovery monitor each serve themselves, so `_lifecycle`
|
|
174
|
+
starts two tasks where it started one, and `AuthManager` no longer has a
|
|
175
|
+
lifecycle of its own.
|
|
176
|
+
|
|
177
|
+
The public surface was cut at the same time, on the rule that nothing returning
|
|
178
|
+
a bare `ProtoOA` message should be public:
|
|
179
|
+
|
|
180
|
+
- `authenticate_app` is called by the client as it connects, and on every
|
|
181
|
+
reconnect. A client that cannot authenticate does not open, and the failure
|
|
182
|
+
arrives as `ApplicationAuthError` thanks to the unwrapping above.
|
|
183
|
+
- `authenticate_account` became `authenticate_trader`, returning nothing. The
|
|
184
|
+
`trigger` argument moved to the internal `establish`, where the refresher and
|
|
185
|
+
the recovery monitor use it.
|
|
186
|
+
- `get_accounts` and `resolve_account_id` moved to `AccountsAPI` as
|
|
187
|
+
`list_by_token` and `resolve_account_id`. They are account queries, not auth.
|
|
188
|
+
- `authenticate_by_trader_login` is gone with no replacement. Resolving a login
|
|
189
|
+
and authenticating an account are two steps, and the convenience of joining
|
|
190
|
+
them cost a second way to do the same thing:
|
|
191
|
+
`await client.auth.authenticate_trader(await store.load(account_id))` is now
|
|
192
|
+
the only shape, restart or not.
|
|
193
|
+
|
|
194
|
+
`APIError.is_token_failure()` replaces the module-level helper, since three
|
|
195
|
+
modules now need to ask.
|
|
196
|
+
|
|
197
|
+
### Flat public surface — `6cc397c`
|
|
198
|
+
|
|
199
|
+
The package exported `CTraderClient`, `ClientConfig` and seven of the fourteen
|
|
200
|
+
enums "for easier access". The seven were an arbitrary subset: a caller reading
|
|
201
|
+
`order.status`, `order.time_in_force`, `deal.status`, `symbol.trading_mode` or
|
|
202
|
+
`account.access_rights` had to switch to `ctrader_api_client.enums` halfway down
|
|
203
|
+
the file. `AccountCredentials`, the one type nobody can avoid constructing, sat
|
|
204
|
+
two modules deep.
|
|
205
|
+
|
|
206
|
+
The top level now carries what a caller names — what you construct, what you
|
|
207
|
+
receive, what you catch — and the subpackages keep what the composition root
|
|
208
|
+
wires. Sixty-eight names: the client and its config, the credentials and the two
|
|
209
|
+
policies and the `TokenStore` protocol, every enum, every model, every event
|
|
210
|
+
type, every exception. `AuthManager`, `SessionStore`, `TokenRefresher`,
|
|
211
|
+
`SessionRecovery`, the emitter/publisher/router and everything under `api/`,
|
|
212
|
+
`connection/` and `_internal/` stay where they are, since the client hands them
|
|
213
|
+
to you already assembled.
|
|
214
|
+
|
|
215
|
+
The result models are exported despite `Account`, `Order`, `Position`, `Symbol`
|
|
216
|
+
and `Event` being names a trading codebase may well use itself. A name you
|
|
217
|
+
receive is a name you annotate, and `import ctrader_api_client as ct` is there
|
|
218
|
+
for anyone with a conflict.
|
|
219
|
+
|
|
220
|
+
Additive: the subpackage paths still work. Importing costs nothing extra either,
|
|
221
|
+
since `from .client import CTraderClient` already pulled the whole graph in.
|
|
222
|
+
|
|
223
|
+
The `Environment` enum was deleted in passing — declared, documented, and
|
|
224
|
+
referenced nowhere. `ClientConfig` takes a `host` string.
|
|
225
|
+
|
|
226
|
+
## Open
|
|
227
|
+
|
|
228
|
+
### Version bump and changelog
|
|
229
|
+
|
|
230
|
+
The lifecycle inversion removes `connect()` and `close()` from the public API,
|
|
231
|
+
and the connection supervisor replaces `Protocol.set_reconnect_handler` /
|
|
232
|
+
`set_disconnect_handler` with `set_listener` and drops
|
|
233
|
+
`AuthManager.handle_connection_lost`. The AuthManager split then removes
|
|
234
|
+
`authenticate_app`, `authenticate_account`, `authenticate_by_trader_login`,
|
|
235
|
+
`get_accounts` and `resolve_account_id` from `client.auth`, adding
|
|
236
|
+
`authenticate_trader` in their place and moving the two queries to
|
|
237
|
+
`client.accounts`. The `TokenStore` commit moved `AuthTrigger` out of
|
|
238
|
+
`auth.trigger` into `enums`, and turned `ReadyEvent.is_reconnect` into a
|
|
239
|
+
property over a new `trigger` field, which breaks anyone constructing the event
|
|
240
|
+
themselves. This needs a bump from `0.6.0` and a note saying so.
|
|
241
|
+
|
|
242
|
+
The flat surface is additive and breaks nobody, but the `Environment` enum it
|
|
243
|
+
deleted was public, so it belongs in the same note. Nothing else on the branch
|
|
244
|
+
breaks a caller.
|
|
245
|
+
|
|
246
|
+
### Smaller things noticed but not acted on
|
|
247
|
+
|
|
248
|
+
- `client.py` is down to 501 lines, nearly all delegation and docstrings.
|
|
249
|
+
- `protocol.py` is 550 lines and does framing, correlation, dispatch and
|
|
250
|
+
reconnection. Splitting it was never discussed; noting it as an observation
|
|
251
|
+
rather than a proposal.
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ctrader-api-client
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: API Client to interact with the cTrader Open API spec
|
|
5
|
+
Author-email: Elio <elioachukri@pm.me>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Natural Language :: English
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Requires-Dist: anyio>=4.13.0
|
|
15
|
+
Requires-Dist: betterproto[compiler]>=1.2.5
|
|
16
|
+
Requires-Dist: pydantic>=2.12.5
|
|
17
|
+
Requires-Dist: tenacity>=9.1.4
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# cTrader API Client
|
|
21
|
+
|
|
22
|
+
A Python client for the cTrader Open API. Provides a high-level async interface for trading operations, market data subscriptions, and account management.
|
|
23
|
+
|
|
24
|
+
Documentation:
|
|
25
|
+
- [Library Docs](https://elioachukri.github.io/ctrader-api-client/)
|
|
26
|
+
- [cTrader Open API Docs](https://help.ctrader.com/open-api/)
|
|
27
|
+
|
|
28
|
+
> Note that this library is in early development. The API may change, and some features may be incomplete. Contributions and feedback are welcome!
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Python 3.12+
|
|
33
|
+
- An activated cTrader Open API application with client ID and secret
|
|
34
|
+
- OAuth tokens for account authentication (see below)
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
**Using uv (recommended):**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv add ctrader-api-client
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Using pip:**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install ctrader-api-client
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import asyncio
|
|
54
|
+
from ctrader_api_client import (
|
|
55
|
+
AccountCredentials,
|
|
56
|
+
ClientConfig,
|
|
57
|
+
CTraderClient,
|
|
58
|
+
ReadyEvent,
|
|
59
|
+
SpotEvent,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
config = ClientConfig(
|
|
63
|
+
client_id="your_client_id",
|
|
64
|
+
client_secret="your_client_secret",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
client = CTraderClient(config)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@client.on(SpotEvent, symbol_id=270) # US500.cash
|
|
71
|
+
async def on_price(event: SpotEvent):
|
|
72
|
+
print(f"Price update: {event.bid}/{event.ask}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def main():
|
|
76
|
+
async with client:
|
|
77
|
+
account_id = await client.accounts.resolve_account_id(
|
|
78
|
+
"your_access_token",
|
|
79
|
+
trader_login=12345678,
|
|
80
|
+
)
|
|
81
|
+
await client.auth.authenticate_trader(
|
|
82
|
+
AccountCredentials(
|
|
83
|
+
account_id=account_id,
|
|
84
|
+
access_token="your_access_token",
|
|
85
|
+
refresh_token="your_refresh_token",
|
|
86
|
+
expires_at=1778617423,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Subscribe once. The client re-applies this after any reconnection.
|
|
91
|
+
await client.market_data.subscribe_spots(account_id, [270])
|
|
92
|
+
|
|
93
|
+
# Keep running to receive events
|
|
94
|
+
await asyncio.Event().wait()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
asyncio.run(main())
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## OAuth Token Generation
|
|
102
|
+
|
|
103
|
+
This library requires OAuth tokens from cTrader. For simple use cases, you can use [ctrader-oauth-fetcher](https://github.com/ElioaChukri/ctrader-oauth-fetcher) to generate tokens:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
uvx ctrader-oauth-fetcher --client-id [ID] --client-secret [SECRET]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This opens a browser for authorization and returns your access token, refresh token, and expiry time.
|
|
110
|
+
|
|
111
|
+
For production applications, implement the OAuth flow according to the [cTrader Open API documentation](https://help.ctrader.com/open-api/).
|
|
112
|
+
|
|
113
|
+
## Features
|
|
114
|
+
|
|
115
|
+
### Authentication
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# The application is authenticated as the client connects.
|
|
119
|
+
|
|
120
|
+
# Discover the account behind a trader login
|
|
121
|
+
account_id = await client.accounts.resolve_account_id("...", trader_login=12345678)
|
|
122
|
+
|
|
123
|
+
# Authenticate a trading account
|
|
124
|
+
await client.auth.authenticate_trader(
|
|
125
|
+
AccountCredentials(
|
|
126
|
+
account_id=account_id,
|
|
127
|
+
access_token="...",
|
|
128
|
+
refresh_token="...",
|
|
129
|
+
expires_at=1778617423,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Tokens are automatically refreshed before expiry. A refresh that fails is
|
|
134
|
+
# retried on the next check rather than dropping the session, and surfaces as a
|
|
135
|
+
# TokenRefreshFailedEvent so a persistently dead refresh token is observable.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Persisting Rotated Tokens
|
|
139
|
+
|
|
140
|
+
Every refresh issues a new access **and** refresh token, and invalidates the old
|
|
141
|
+
pair immediately. Pass a `TokenStore` and the client writes each new pair through
|
|
142
|
+
as it is issued:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from ctrader_api_client import AccountCredentials, CTraderClient, TokenStore
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class PostgresTokenStore(TokenStore):
|
|
149
|
+
def __init__(self, pool):
|
|
150
|
+
self._pool = pool
|
|
151
|
+
|
|
152
|
+
async def save(self, credentials: AccountCredentials) -> None:
|
|
153
|
+
await self._pool.execute(
|
|
154
|
+
"""
|
|
155
|
+
INSERT INTO ctrader_tokens (account_id, access_token, refresh_token, expires_at)
|
|
156
|
+
VALUES ($1, $2, $3, $4)
|
|
157
|
+
ON CONFLICT (account_id) DO UPDATE SET
|
|
158
|
+
access_token = EXCLUDED.access_token,
|
|
159
|
+
refresh_token = EXCLUDED.refresh_token,
|
|
160
|
+
expires_at = EXCLUDED.expires_at
|
|
161
|
+
""",
|
|
162
|
+
credentials.account_id,
|
|
163
|
+
credentials.access_token,
|
|
164
|
+
credentials.refresh_token,
|
|
165
|
+
credentials.expires_at,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Not part of the protocol. The client never reads the store; you do.
|
|
169
|
+
async def load(self, account_id: int) -> AccountCredentials | None:
|
|
170
|
+
row = await self._pool.fetchrow(
|
|
171
|
+
"SELECT access_token, refresh_token, expires_at FROM ctrader_tokens WHERE account_id = $1",
|
|
172
|
+
account_id,
|
|
173
|
+
)
|
|
174
|
+
if row is None:
|
|
175
|
+
return None
|
|
176
|
+
return AccountCredentials(account_id=account_id, **dict(row))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
client = CTraderClient(config, token_store=PostgresTokenStore(pool))
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The save happens before the new token is put to use. A save that raises aborts
|
|
183
|
+
that refresh, which is reported as a `TokenRefreshFailedEvent` and retried on the
|
|
184
|
+
next check interval, so a transient storage outage recovers on its own.
|
|
185
|
+
|
|
186
|
+
The protocol is write-only, because writing is the half the client has to do for
|
|
187
|
+
you: rotation happens mid-session, at a moment you cannot observe. Reading back is
|
|
188
|
+
yours, since only you know which accounts a given process is responsible for:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
store = PostgresTokenStore(pool)
|
|
192
|
+
client = CTraderClient(config, token_store=store)
|
|
193
|
+
|
|
194
|
+
async with client:
|
|
195
|
+
stored = await store.load(account_id)
|
|
196
|
+
if stored is None:
|
|
197
|
+
stored = AccountCredentials(
|
|
198
|
+
account_id=account_id,
|
|
199
|
+
access_token="your_access_token",
|
|
200
|
+
refresh_token="your_refresh_token",
|
|
201
|
+
expires_at=1778617423,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
await client.auth.authenticate_trader(stored)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Market Data
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
# Subscribe to spot prices
|
|
211
|
+
await client.market_data.subscribe_spots(account_id, [symbol_id])
|
|
212
|
+
|
|
213
|
+
# Subscribe to candles
|
|
214
|
+
await client.market_data.subscribe_trendbars(account_id, symbol_id, TrendbarPeriod.M1)
|
|
215
|
+
|
|
216
|
+
# Get historical data
|
|
217
|
+
bars = await client.market_data.get_trendbars(
|
|
218
|
+
account_id, symbol_id, TrendbarPeriod.H1, from_ts, to_ts
|
|
219
|
+
)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Trading
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from ctrader_api_client import ClosePositionRequest, NewOrderRequest, OrderSide, OrderType
|
|
226
|
+
|
|
227
|
+
# Place a market order
|
|
228
|
+
request = NewOrderRequest(
|
|
229
|
+
symbol_id=symbol_id,
|
|
230
|
+
order_type=OrderType.MARKET,
|
|
231
|
+
side=OrderSide.BUY,
|
|
232
|
+
volume=100000, # 1 lot in cents
|
|
233
|
+
)
|
|
234
|
+
result = await client.trading.place_order(account_id, request)
|
|
235
|
+
|
|
236
|
+
# Get open positions
|
|
237
|
+
positions = await client.trading.get_open_positions(account_id)
|
|
238
|
+
|
|
239
|
+
# Close a position
|
|
240
|
+
close_position = ClosePositionRequest(
|
|
241
|
+
position_id=position_id,
|
|
242
|
+
volume=100000, # Close full volume
|
|
243
|
+
)
|
|
244
|
+
await client.trading.close_position(account_id, close_position)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Event Handling
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from ctrader_api_client import (
|
|
251
|
+
ExecutionEvent,
|
|
252
|
+
ReadyEvent,
|
|
253
|
+
ReconnectedEvent,
|
|
254
|
+
SpotEvent,
|
|
255
|
+
SubscriptionRestoreFailedEvent,
|
|
256
|
+
TokenRefreshFailedEvent,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Price updates
|
|
260
|
+
@client.on(SpotEvent, symbol_id=270)
|
|
261
|
+
async def on_spot(event: SpotEvent):
|
|
262
|
+
print(f"{event.bid}/{event.ask}")
|
|
263
|
+
|
|
264
|
+
# Order executions
|
|
265
|
+
@client.on(ExecutionEvent, account_id=account_id)
|
|
266
|
+
async def on_execution(event: ExecutionEvent):
|
|
267
|
+
print(f"Order {event.order_id}: {event.execution_type}")
|
|
268
|
+
|
|
269
|
+
# Account ready (fires on initial auth, after reconnection, and after account-disconnect recovery)
|
|
270
|
+
# Subscriptions are already restored by this point; use it to reconcile your own state.
|
|
271
|
+
@client.on(ReadyEvent)
|
|
272
|
+
async def on_ready(event: ReadyEvent):
|
|
273
|
+
if event.is_reconnect:
|
|
274
|
+
positions = await client.trading.get_open_positions(event.account_id)
|
|
275
|
+
|
|
276
|
+
# Connection restored
|
|
277
|
+
@client.on(ReconnectedEvent)
|
|
278
|
+
async def on_reconnected(event: ReconnectedEvent):
|
|
279
|
+
print(f"Reconnected, restored accounts: {event.restored_accounts}")
|
|
280
|
+
|
|
281
|
+
# Token refresh failed (retried automatically; a repeating event means the
|
|
282
|
+
# refresh token is no longer usable and the account must be re-authorized)
|
|
283
|
+
@client.on(TokenRefreshFailedEvent, account_id=account_id)
|
|
284
|
+
async def on_refresh_failed(event: TokenRefreshFailedEvent):
|
|
285
|
+
print(f"Token refresh failed for {event.account_id}: {event.error}")
|
|
286
|
+
|
|
287
|
+
# Market data could not be re-applied after a reconnection (retried on the next one)
|
|
288
|
+
@client.on(SubscriptionRestoreFailedEvent, account_id=account_id)
|
|
289
|
+
async def on_restore_failed(event: SubscriptionRestoreFailedEvent):
|
|
290
|
+
print(f"Account {event.account_id} is missing market data: {event.error}")
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Symbols
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
# List all symbols
|
|
297
|
+
symbols = await client.symbols.list_all(account_id)
|
|
298
|
+
|
|
299
|
+
# Search by name
|
|
300
|
+
results = await client.symbols.search(account_id, "EUR")
|
|
301
|
+
|
|
302
|
+
# Get specific symbol
|
|
303
|
+
symbol = await client.symbols.get_by_id(account_id, symbol_id)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Account Information
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
# Get account details
|
|
310
|
+
account = await client.accounts.get_trader(account_id)
|
|
311
|
+
print(f"Balance: {account.balance}")
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Automatic Reconnection
|
|
315
|
+
|
|
316
|
+
The client automatically handles connection drops:
|
|
317
|
+
|
|
318
|
+
1. Reconnects with exponential backoff
|
|
319
|
+
2. Re-authenticates the app and all accounts
|
|
320
|
+
3. Re-applies each account's market data subscriptions
|
|
321
|
+
4. Emits `ReadyEvent` for each restored account
|
|
322
|
+
5. Emits `ReconnectedEvent` with summary of restored/failed accounts
|
|
323
|
+
|
|
324
|
+
It also handles **server-side account disconnects** (e.g. a broker dropping the
|
|
325
|
+
account session over the weekend while the connection stays up): the account is
|
|
326
|
+
re-authenticated on the existing connection with backoff until it succeeds, its
|
|
327
|
+
subscriptions are re-applied, and then a `ReadyEvent` is emitted. Account
|
|
328
|
+
authorization is observable via `client.is_account_authorized(account_id)`,
|
|
329
|
+
which is distinct from the transport-level `client.is_connected`.
|
|
330
|
+
|
|
331
|
+
Subscribe once, when you first authenticate. The client remembers what each
|
|
332
|
+
account asked for and re-applies it before announcing the account as ready, so
|
|
333
|
+
do not re-subscribe from a `ReadyEvent` handler — the server rejects a duplicate
|
|
334
|
+
subscription. If restoration fails it stops at the first failure and emits a
|
|
335
|
+
`SubscriptionRestoreFailedEvent`, keeping the intent so the next reconnection
|
|
336
|
+
tries again.
|
|
337
|
+
|
|
338
|
+
## Configuration
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
from ctrader_api_client import ClientConfig, ReauthPolicy, RefreshPolicy
|
|
342
|
+
|
|
343
|
+
config = ClientConfig(
|
|
344
|
+
client_id="your_client_id",
|
|
345
|
+
client_secret="your_client_secret",
|
|
346
|
+
|
|
347
|
+
# Connection settings
|
|
348
|
+
host="live.ctraderapi.com", # or "demo.ctraderapi.com"
|
|
349
|
+
port=5035,
|
|
350
|
+
use_ssl=True,
|
|
351
|
+
|
|
352
|
+
# Timeouts
|
|
353
|
+
heartbeat_interval=10.0,
|
|
354
|
+
heartbeat_timeout=30.0, # Or 0 to disable server heartbeat checks
|
|
355
|
+
request_timeout=30.0,
|
|
356
|
+
|
|
357
|
+
# Reconnection
|
|
358
|
+
reconnect_attempts=5,
|
|
359
|
+
reconnect_min_wait=1.0,
|
|
360
|
+
reconnect_max_wait=60.0,
|
|
361
|
+
|
|
362
|
+
# Token refresh: when to refresh access tokens and how hard to retry
|
|
363
|
+
refresh_policy=RefreshPolicy(
|
|
364
|
+
buffer_seconds=300.0, # refresh this long before expiry
|
|
365
|
+
check_interval=60.0, # how often to check for expiring tokens
|
|
366
|
+
retry_attempts=3,
|
|
367
|
+
retry_min_wait=1.0,
|
|
368
|
+
retry_max_wait=30.0,
|
|
369
|
+
),
|
|
370
|
+
|
|
371
|
+
# Session recovery: backoff for re-establishing sessions the server dropped
|
|
372
|
+
reauth_policy=ReauthPolicy(
|
|
373
|
+
min_wait=1.0,
|
|
374
|
+
max_wait=60.0,
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
```
|