ctrader-api-client 0.6.0__tar.gz → 0.7.1__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.6.0 → ctrader_api_client-0.7.1}/.claude/settings.local.json +2 -1
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/PKG-INFO +125 -34
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/README.md +124 -33
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/docs/api/accounts.md +8 -4
- ctrader_api_client-0.7.1/docs/api/client.md +133 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/docs/api/enums.md +1 -7
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/docs/api/events.md +34 -12
- ctrader_api_client-0.7.1/docs/api/exceptions.md +213 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/docs/api/market-data.md +26 -15
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/docs/api/models.md +2 -3
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/docs/api/trading.md +3 -4
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/docs/getting-started.md +132 -35
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/docs/index.md +14 -13
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/mkdocs.yml +1 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/pyproject.toml +1 -1
- ctrader_api_client-0.7.1/src/ctrader_api_client/__init__.py +190 -0
- ctrader_api_client-0.7.1/src/ctrader_api_client/api/accounts.py +134 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/api/market_data.py +88 -0
- ctrader_api_client-0.7.1/src/ctrader_api_client/auth/__init__.py +26 -0
- ctrader_api_client-0.7.1/src/ctrader_api_client/auth/_recovery.py +299 -0
- ctrader_api_client-0.7.1/src/ctrader_api_client/auth/_refresh.py +203 -0
- ctrader_api_client-0.7.1/src/ctrader_api_client/auth/_session.py +180 -0
- ctrader_api_client-0.7.1/src/ctrader_api_client/auth/manager.py +259 -0
- ctrader_api_client-0.7.1/src/ctrader_api_client/auth/store.py +35 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/client.py +143 -241
- ctrader_api_client-0.7.1/src/ctrader_api_client/composition.py +165 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/connection/__init__.py +4 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/connection/heartbeat.py +31 -24
- ctrader_api_client-0.7.1/src/ctrader_api_client/connection/listener.py +17 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/connection/protocol.py +93 -69
- ctrader_api_client-0.7.1/src/ctrader_api_client/connection/supervisor.py +124 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/enums.py +12 -5
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/events/__init__.py +4 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/events/emitter.py +2 -22
- ctrader_api_client-0.7.1/src/ctrader_api_client/events/publisher.py +19 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/events/router.py +47 -69
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/events/types.py +41 -9
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/exceptions.py +26 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/api/conftest.py +8 -3
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/api/test_reference_data.py +100 -1
- ctrader_api_client-0.7.1/tests/behavior/api/test_subscription_restore.py +206 -0
- ctrader_api_client-0.7.1/tests/behavior/auth/conftest.py +120 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/auth/test_authentication.py +59 -149
- ctrader_api_client-0.7.1/tests/behavior/auth/test_token_lifecycle.py +465 -0
- ctrader_api_client-0.7.1/tests/behavior/connection/conftest.py +24 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/connection/test_framing.py +20 -14
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/connection/test_heartbeat.py +42 -11
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/connection/test_reconnection.py +59 -16
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/connection/test_request_response.py +8 -8
- ctrader_api_client-0.7.1/tests/behavior/events/conftest.py +32 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/events/test_routing.py +79 -3
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/events/test_subscriptions.py +0 -47
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/test_client.py +124 -28
- ctrader_api_client-0.7.1/tests/conftest.py +160 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/harness/__init__.py +12 -1
- ctrader_api_client-0.7.1/tests/harness/recorder.py +163 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/harness/stub_protocol.py +35 -1
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/uv.lock +1 -1
- ctrader_api_client-0.6.0/docs/api/client.md +0 -65
- ctrader_api_client-0.6.0/src/ctrader_api_client/__init__.py +0 -66
- ctrader_api_client-0.6.0/src/ctrader_api_client/api/accounts.py +0 -50
- ctrader_api_client-0.6.0/src/ctrader_api_client/auth/__init__.py +0 -19
- ctrader_api_client-0.6.0/src/ctrader_api_client/auth/_session.py +0 -59
- ctrader_api_client-0.6.0/src/ctrader_api_client/auth/manager.py +0 -713
- ctrader_api_client-0.6.0/src/ctrader_api_client/auth/trigger.py +0 -17
- ctrader_api_client-0.6.0/tests/behavior/auth/conftest.py +0 -52
- ctrader_api_client-0.6.0/tests/behavior/auth/test_token_lifecycle.py +0 -345
- ctrader_api_client-0.6.0/tests/behavior/events/conftest.py +0 -35
- ctrader_api_client-0.6.0/tests/conftest.py +0 -102
- ctrader_api_client-0.6.0/tests/harness/recorder.py +0 -73
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/.github/workflows/docs.yml +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/.gitignore +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/.pre-commit-config.yaml +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/.python-version +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/Justfile +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/LICENSE +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/docs/api/symbols.md +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/protos/SOURCE +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/protos/VERSION +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/protos/update.sh +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/protos/vendor/OpenApiCommonMessages.proto +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/protos/vendor/OpenApiMessages.proto +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/protos/vendor/OpenApiModelMessages.proto +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/scripts/fix_proto_imports.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/_internal/__init__.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/_internal/clock.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/_internal/conversions.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/_internal/messages.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/_internal/serialization.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/api/__init__.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/api/_base.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/api/symbols.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/api/trading.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/auth/credentials.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/auth/policy.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/config.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/connection/transport.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/events/_execution.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/models/__init__.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/models/_base.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/models/account.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/models/deal.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/models/market_data.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/models/order.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/models/position.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/models/requests.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/models/symbol.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/src/ctrader_api_client/py.typed +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/__init__.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/__init__.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/api/__init__.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/api/test_market_data.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/api/test_trading.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/auth/__init__.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/connection/__init__.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/connection/test_event_dispatch.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/behavior/events/__init__.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/harness/clock.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/harness/factories.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/harness/server.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/harness/signals.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/harness/wire.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/unit/__init__.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/unit/test_api_errors.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/unit/test_market_data_conversion.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/unit/test_request_conversion.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/unit/test_symbol_conversion.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/unit/test_trading_records.py +0 -0
- {ctrader_api_client-0.6.0 → ctrader_api_client-0.7.1}/tests/unit/test_wire_format.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Bash(git -C \"/home/tek/Desktop/Algo Trading/Live Trading/cTrader/ctrader-api-client\" log --oneline -10)"
|
|
4
|
+
"Bash(git -C \"/home/tek/Desktop/Algo Trading/Live Trading/cTrader/ctrader-api-client\" log --oneline -10)",
|
|
5
|
+
"Bash(git add *)"
|
|
5
6
|
]
|
|
6
7
|
}
|
|
7
8
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctrader-api-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
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
|
|
@@ -51,8 +51,13 @@ pip install ctrader-api-client
|
|
|
51
51
|
|
|
52
52
|
```python
|
|
53
53
|
import asyncio
|
|
54
|
-
from ctrader_api_client import
|
|
55
|
-
|
|
54
|
+
from ctrader_api_client import (
|
|
55
|
+
AccountCredentials,
|
|
56
|
+
ClientConfig,
|
|
57
|
+
CTraderClient,
|
|
58
|
+
ReadyEvent,
|
|
59
|
+
SpotEvent,
|
|
60
|
+
)
|
|
56
61
|
|
|
57
62
|
config = ClientConfig(
|
|
58
63
|
client_id="your_client_id",
|
|
@@ -67,21 +72,23 @@ async def on_price(event: SpotEvent):
|
|
|
67
72
|
print(f"Price update: {event.bid}/{event.ask}")
|
|
68
73
|
|
|
69
74
|
|
|
70
|
-
@client.on(ReadyEvent)
|
|
71
|
-
async def on_ready(event: ReadyEvent):
|
|
72
|
-
"""Called when account is authenticated and ready."""
|
|
73
|
-
await client.market_data.subscribe_spots(event.account_id, [270])
|
|
74
|
-
|
|
75
|
-
|
|
76
75
|
async def main():
|
|
77
76
|
async with client:
|
|
78
|
-
await client.
|
|
79
|
-
|
|
77
|
+
account_id = await client.accounts.resolve_account_id(
|
|
78
|
+
"your_access_token",
|
|
80
79
|
trader_login=12345678,
|
|
81
|
-
access_token="your_access_token",
|
|
82
|
-
refresh_token="your_refresh_token",
|
|
83
|
-
expires_at=1778617423,
|
|
84
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])
|
|
85
92
|
|
|
86
93
|
# Keep running to receive events
|
|
87
94
|
await asyncio.Event().wait()
|
|
@@ -108,15 +115,19 @@ For production applications, implement the OAuth flow according to the [cTrader
|
|
|
108
115
|
### Authentication
|
|
109
116
|
|
|
110
117
|
```python
|
|
111
|
-
#
|
|
112
|
-
|
|
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)
|
|
113
122
|
|
|
114
123
|
# Authenticate a trading account
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
await client.auth.authenticate_trader(
|
|
125
|
+
AccountCredentials(
|
|
126
|
+
account_id=account_id,
|
|
127
|
+
access_token="...",
|
|
128
|
+
refresh_token="...",
|
|
129
|
+
expires_at=1778617423,
|
|
130
|
+
)
|
|
120
131
|
)
|
|
121
132
|
|
|
122
133
|
# Tokens are automatically refreshed before expiry. A refresh that fails is
|
|
@@ -124,6 +135,75 @@ creds = await client.auth.authenticate_by_trader_login(
|
|
|
124
135
|
# TokenRefreshFailedEvent so a persistently dead refresh token is observable.
|
|
125
136
|
```
|
|
126
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
|
+
|
|
127
207
|
### Market Data
|
|
128
208
|
|
|
129
209
|
```python
|
|
@@ -142,8 +222,7 @@ bars = await client.market_data.get_trendbars(
|
|
|
142
222
|
### Trading
|
|
143
223
|
|
|
144
224
|
```python
|
|
145
|
-
from ctrader_api_client
|
|
146
|
-
from ctrader_api_client.enums import OrderType, OrderSide
|
|
225
|
+
from ctrader_api_client import ClosePositionRequest, NewOrderRequest, OrderSide, OrderType
|
|
147
226
|
|
|
148
227
|
# Place a market order
|
|
149
228
|
request = NewOrderRequest(
|
|
@@ -168,11 +247,12 @@ await client.trading.close_position(account_id, close_position)
|
|
|
168
247
|
### Event Handling
|
|
169
248
|
|
|
170
249
|
```python
|
|
171
|
-
from ctrader_api_client
|
|
172
|
-
SpotEvent,
|
|
250
|
+
from ctrader_api_client import (
|
|
173
251
|
ExecutionEvent,
|
|
174
252
|
ReadyEvent,
|
|
175
253
|
ReconnectedEvent,
|
|
254
|
+
SpotEvent,
|
|
255
|
+
SubscriptionRestoreFailedEvent,
|
|
176
256
|
TokenRefreshFailedEvent,
|
|
177
257
|
)
|
|
178
258
|
|
|
@@ -187,10 +267,11 @@ async def on_execution(event: ExecutionEvent):
|
|
|
187
267
|
print(f"Order {event.order_id}: {event.execution_type}")
|
|
188
268
|
|
|
189
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.
|
|
190
271
|
@client.on(ReadyEvent)
|
|
191
272
|
async def on_ready(event: ReadyEvent):
|
|
192
|
-
|
|
193
|
-
|
|
273
|
+
if event.is_reconnect:
|
|
274
|
+
positions = await client.trading.get_open_positions(event.account_id)
|
|
194
275
|
|
|
195
276
|
# Connection restored
|
|
196
277
|
@client.on(ReconnectedEvent)
|
|
@@ -202,6 +283,11 @@ async def on_reconnected(event: ReconnectedEvent):
|
|
|
202
283
|
@client.on(TokenRefreshFailedEvent, account_id=account_id)
|
|
203
284
|
async def on_refresh_failed(event: TokenRefreshFailedEvent):
|
|
204
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}")
|
|
205
291
|
```
|
|
206
292
|
|
|
207
293
|
### Symbols
|
|
@@ -231,23 +317,28 @@ The client automatically handles connection drops:
|
|
|
231
317
|
|
|
232
318
|
1. Reconnects with exponential backoff
|
|
233
319
|
2. Re-authenticates the app and all accounts
|
|
234
|
-
3.
|
|
235
|
-
4. Emits `
|
|
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
|
|
236
323
|
|
|
237
324
|
It also handles **server-side account disconnects** (e.g. a broker dropping the
|
|
238
325
|
account session over the weekend while the connection stays up): the account is
|
|
239
|
-
re-authenticated on the existing connection with backoff until it succeeds,
|
|
240
|
-
a `ReadyEvent` is emitted
|
|
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
|
|
241
328
|
authorization is observable via `client.is_account_authorized(account_id)`,
|
|
242
329
|
which is distinct from the transport-level `client.is_connected`.
|
|
243
330
|
|
|
244
|
-
|
|
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.
|
|
245
337
|
|
|
246
338
|
## Configuration
|
|
247
339
|
|
|
248
340
|
```python
|
|
249
|
-
from ctrader_api_client import ClientConfig
|
|
250
|
-
from ctrader_api_client.auth import ReauthPolicy, RefreshPolicy
|
|
341
|
+
from ctrader_api_client import ClientConfig, ReauthPolicy, RefreshPolicy
|
|
251
342
|
|
|
252
343
|
config = ClientConfig(
|
|
253
344
|
client_id="your_client_id",
|
|
@@ -32,8 +32,13 @@ pip install ctrader-api-client
|
|
|
32
32
|
|
|
33
33
|
```python
|
|
34
34
|
import asyncio
|
|
35
|
-
from ctrader_api_client import
|
|
36
|
-
|
|
35
|
+
from ctrader_api_client import (
|
|
36
|
+
AccountCredentials,
|
|
37
|
+
ClientConfig,
|
|
38
|
+
CTraderClient,
|
|
39
|
+
ReadyEvent,
|
|
40
|
+
SpotEvent,
|
|
41
|
+
)
|
|
37
42
|
|
|
38
43
|
config = ClientConfig(
|
|
39
44
|
client_id="your_client_id",
|
|
@@ -48,21 +53,23 @@ async def on_price(event: SpotEvent):
|
|
|
48
53
|
print(f"Price update: {event.bid}/{event.ask}")
|
|
49
54
|
|
|
50
55
|
|
|
51
|
-
@client.on(ReadyEvent)
|
|
52
|
-
async def on_ready(event: ReadyEvent):
|
|
53
|
-
"""Called when account is authenticated and ready."""
|
|
54
|
-
await client.market_data.subscribe_spots(event.account_id, [270])
|
|
55
|
-
|
|
56
|
-
|
|
57
56
|
async def main():
|
|
58
57
|
async with client:
|
|
59
|
-
await client.
|
|
60
|
-
|
|
58
|
+
account_id = await client.accounts.resolve_account_id(
|
|
59
|
+
"your_access_token",
|
|
61
60
|
trader_login=12345678,
|
|
62
|
-
access_token="your_access_token",
|
|
63
|
-
refresh_token="your_refresh_token",
|
|
64
|
-
expires_at=1778617423,
|
|
65
61
|
)
|
|
62
|
+
await client.auth.authenticate_trader(
|
|
63
|
+
AccountCredentials(
|
|
64
|
+
account_id=account_id,
|
|
65
|
+
access_token="your_access_token",
|
|
66
|
+
refresh_token="your_refresh_token",
|
|
67
|
+
expires_at=1778617423,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Subscribe once. The client re-applies this after any reconnection.
|
|
72
|
+
await client.market_data.subscribe_spots(account_id, [270])
|
|
66
73
|
|
|
67
74
|
# Keep running to receive events
|
|
68
75
|
await asyncio.Event().wait()
|
|
@@ -89,15 +96,19 @@ For production applications, implement the OAuth flow according to the [cTrader
|
|
|
89
96
|
### Authentication
|
|
90
97
|
|
|
91
98
|
```python
|
|
92
|
-
#
|
|
93
|
-
|
|
99
|
+
# The application is authenticated as the client connects.
|
|
100
|
+
|
|
101
|
+
# Discover the account behind a trader login
|
|
102
|
+
account_id = await client.accounts.resolve_account_id("...", trader_login=12345678)
|
|
94
103
|
|
|
95
104
|
# Authenticate a trading account
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
await client.auth.authenticate_trader(
|
|
106
|
+
AccountCredentials(
|
|
107
|
+
account_id=account_id,
|
|
108
|
+
access_token="...",
|
|
109
|
+
refresh_token="...",
|
|
110
|
+
expires_at=1778617423,
|
|
111
|
+
)
|
|
101
112
|
)
|
|
102
113
|
|
|
103
114
|
# Tokens are automatically refreshed before expiry. A refresh that fails is
|
|
@@ -105,6 +116,75 @@ creds = await client.auth.authenticate_by_trader_login(
|
|
|
105
116
|
# TokenRefreshFailedEvent so a persistently dead refresh token is observable.
|
|
106
117
|
```
|
|
107
118
|
|
|
119
|
+
### Persisting Rotated Tokens
|
|
120
|
+
|
|
121
|
+
Every refresh issues a new access **and** refresh token, and invalidates the old
|
|
122
|
+
pair immediately. Pass a `TokenStore` and the client writes each new pair through
|
|
123
|
+
as it is issued:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from ctrader_api_client import AccountCredentials, CTraderClient, TokenStore
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class PostgresTokenStore(TokenStore):
|
|
130
|
+
def __init__(self, pool):
|
|
131
|
+
self._pool = pool
|
|
132
|
+
|
|
133
|
+
async def save(self, credentials: AccountCredentials) -> None:
|
|
134
|
+
await self._pool.execute(
|
|
135
|
+
"""
|
|
136
|
+
INSERT INTO ctrader_tokens (account_id, access_token, refresh_token, expires_at)
|
|
137
|
+
VALUES ($1, $2, $3, $4)
|
|
138
|
+
ON CONFLICT (account_id) DO UPDATE SET
|
|
139
|
+
access_token = EXCLUDED.access_token,
|
|
140
|
+
refresh_token = EXCLUDED.refresh_token,
|
|
141
|
+
expires_at = EXCLUDED.expires_at
|
|
142
|
+
""",
|
|
143
|
+
credentials.account_id,
|
|
144
|
+
credentials.access_token,
|
|
145
|
+
credentials.refresh_token,
|
|
146
|
+
credentials.expires_at,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Not part of the protocol. The client never reads the store; you do.
|
|
150
|
+
async def load(self, account_id: int) -> AccountCredentials | None:
|
|
151
|
+
row = await self._pool.fetchrow(
|
|
152
|
+
"SELECT access_token, refresh_token, expires_at FROM ctrader_tokens WHERE account_id = $1",
|
|
153
|
+
account_id,
|
|
154
|
+
)
|
|
155
|
+
if row is None:
|
|
156
|
+
return None
|
|
157
|
+
return AccountCredentials(account_id=account_id, **dict(row))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
client = CTraderClient(config, token_store=PostgresTokenStore(pool))
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The save happens before the new token is put to use. A save that raises aborts
|
|
164
|
+
that refresh, which is reported as a `TokenRefreshFailedEvent` and retried on the
|
|
165
|
+
next check interval, so a transient storage outage recovers on its own.
|
|
166
|
+
|
|
167
|
+
The protocol is write-only, because writing is the half the client has to do for
|
|
168
|
+
you: rotation happens mid-session, at a moment you cannot observe. Reading back is
|
|
169
|
+
yours, since only you know which accounts a given process is responsible for:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
store = PostgresTokenStore(pool)
|
|
173
|
+
client = CTraderClient(config, token_store=store)
|
|
174
|
+
|
|
175
|
+
async with client:
|
|
176
|
+
stored = await store.load(account_id)
|
|
177
|
+
if stored is None:
|
|
178
|
+
stored = AccountCredentials(
|
|
179
|
+
account_id=account_id,
|
|
180
|
+
access_token="your_access_token",
|
|
181
|
+
refresh_token="your_refresh_token",
|
|
182
|
+
expires_at=1778617423,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
await client.auth.authenticate_trader(stored)
|
|
186
|
+
```
|
|
187
|
+
|
|
108
188
|
### Market Data
|
|
109
189
|
|
|
110
190
|
```python
|
|
@@ -123,8 +203,7 @@ bars = await client.market_data.get_trendbars(
|
|
|
123
203
|
### Trading
|
|
124
204
|
|
|
125
205
|
```python
|
|
126
|
-
from ctrader_api_client
|
|
127
|
-
from ctrader_api_client.enums import OrderType, OrderSide
|
|
206
|
+
from ctrader_api_client import ClosePositionRequest, NewOrderRequest, OrderSide, OrderType
|
|
128
207
|
|
|
129
208
|
# Place a market order
|
|
130
209
|
request = NewOrderRequest(
|
|
@@ -149,11 +228,12 @@ await client.trading.close_position(account_id, close_position)
|
|
|
149
228
|
### Event Handling
|
|
150
229
|
|
|
151
230
|
```python
|
|
152
|
-
from ctrader_api_client
|
|
153
|
-
SpotEvent,
|
|
231
|
+
from ctrader_api_client import (
|
|
154
232
|
ExecutionEvent,
|
|
155
233
|
ReadyEvent,
|
|
156
234
|
ReconnectedEvent,
|
|
235
|
+
SpotEvent,
|
|
236
|
+
SubscriptionRestoreFailedEvent,
|
|
157
237
|
TokenRefreshFailedEvent,
|
|
158
238
|
)
|
|
159
239
|
|
|
@@ -168,10 +248,11 @@ async def on_execution(event: ExecutionEvent):
|
|
|
168
248
|
print(f"Order {event.order_id}: {event.execution_type}")
|
|
169
249
|
|
|
170
250
|
# Account ready (fires on initial auth, after reconnection, and after account-disconnect recovery)
|
|
251
|
+
# Subscriptions are already restored by this point; use it to reconcile your own state.
|
|
171
252
|
@client.on(ReadyEvent)
|
|
172
253
|
async def on_ready(event: ReadyEvent):
|
|
173
|
-
|
|
174
|
-
|
|
254
|
+
if event.is_reconnect:
|
|
255
|
+
positions = await client.trading.get_open_positions(event.account_id)
|
|
175
256
|
|
|
176
257
|
# Connection restored
|
|
177
258
|
@client.on(ReconnectedEvent)
|
|
@@ -183,6 +264,11 @@ async def on_reconnected(event: ReconnectedEvent):
|
|
|
183
264
|
@client.on(TokenRefreshFailedEvent, account_id=account_id)
|
|
184
265
|
async def on_refresh_failed(event: TokenRefreshFailedEvent):
|
|
185
266
|
print(f"Token refresh failed for {event.account_id}: {event.error}")
|
|
267
|
+
|
|
268
|
+
# Market data could not be re-applied after a reconnection (retried on the next one)
|
|
269
|
+
@client.on(SubscriptionRestoreFailedEvent, account_id=account_id)
|
|
270
|
+
async def on_restore_failed(event: SubscriptionRestoreFailedEvent):
|
|
271
|
+
print(f"Account {event.account_id} is missing market data: {event.error}")
|
|
186
272
|
```
|
|
187
273
|
|
|
188
274
|
### Symbols
|
|
@@ -212,23 +298,28 @@ The client automatically handles connection drops:
|
|
|
212
298
|
|
|
213
299
|
1. Reconnects with exponential backoff
|
|
214
300
|
2. Re-authenticates the app and all accounts
|
|
215
|
-
3.
|
|
216
|
-
4. Emits `
|
|
301
|
+
3. Re-applies each account's market data subscriptions
|
|
302
|
+
4. Emits `ReadyEvent` for each restored account
|
|
303
|
+
5. Emits `ReconnectedEvent` with summary of restored/failed accounts
|
|
217
304
|
|
|
218
305
|
It also handles **server-side account disconnects** (e.g. a broker dropping the
|
|
219
306
|
account session over the weekend while the connection stays up): the account is
|
|
220
|
-
re-authenticated on the existing connection with backoff until it succeeds,
|
|
221
|
-
a `ReadyEvent` is emitted
|
|
307
|
+
re-authenticated on the existing connection with backoff until it succeeds, its
|
|
308
|
+
subscriptions are re-applied, and then a `ReadyEvent` is emitted. Account
|
|
222
309
|
authorization is observable via `client.is_account_authorized(account_id)`,
|
|
223
310
|
which is distinct from the transport-level `client.is_connected`.
|
|
224
311
|
|
|
225
|
-
|
|
312
|
+
Subscribe once, when you first authenticate. The client remembers what each
|
|
313
|
+
account asked for and re-applies it before announcing the account as ready, so
|
|
314
|
+
do not re-subscribe from a `ReadyEvent` handler — the server rejects a duplicate
|
|
315
|
+
subscription. If restoration fails it stops at the first failure and emits a
|
|
316
|
+
`SubscriptionRestoreFailedEvent`, keeping the intent so the next reconnection
|
|
317
|
+
tries again.
|
|
226
318
|
|
|
227
319
|
## Configuration
|
|
228
320
|
|
|
229
321
|
```python
|
|
230
|
-
from ctrader_api_client import ClientConfig
|
|
231
|
-
from ctrader_api_client.auth import ReauthPolicy, RefreshPolicy
|
|
322
|
+
from ctrader_api_client import ClientConfig, ReauthPolicy, RefreshPolicy
|
|
232
323
|
|
|
233
324
|
config = ClientConfig(
|
|
234
325
|
client_id="your_client_id",
|
|
@@ -11,6 +11,8 @@ Access via `client.accounts`.
|
|
|
11
11
|
show_source: false
|
|
12
12
|
members:
|
|
13
13
|
- get_trader
|
|
14
|
+
- list_by_token
|
|
15
|
+
- resolve_account_id
|
|
14
16
|
|
|
15
17
|
## Usage Examples
|
|
16
18
|
|
|
@@ -27,15 +29,17 @@ print(f"Broker name: {account.broker_name}")
|
|
|
27
29
|
|
|
28
30
|
## Account Discovery
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
The accounts an access token covers can be listed without authenticating any of
|
|
33
|
+
them:
|
|
31
34
|
|
|
32
35
|
```python
|
|
33
36
|
# Get all accounts associated with a token
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
for acc in accounts:
|
|
37
|
+
for acc in await client.accounts.list_by_token(access_token):
|
|
37
38
|
print(f"Login: {acc.trader_login}, Account ID: {acc.account_id}")
|
|
38
39
|
print(f" Live: {acc.is_live}, Broker: {acc.broker_name}")
|
|
40
|
+
|
|
41
|
+
# Or resolve a single login straight to its account ID
|
|
42
|
+
account_id = await client.accounts.resolve_account_id(access_token, trader_login=12345678)
|
|
39
43
|
```
|
|
40
44
|
|
|
41
45
|
## Related
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Client
|
|
2
|
+
|
|
3
|
+
The main entry point for interacting with the cTrader API.
|
|
4
|
+
|
|
5
|
+
A client is used inside `async with`. The block owns the connection and the
|
|
6
|
+
background tasks that keep it alive, so leaving it — normally or by exception —
|
|
7
|
+
winds them down, and a background task that dies is raised at the end of the
|
|
8
|
+
block rather than discovered later.
|
|
9
|
+
|
|
10
|
+
## CTraderClient
|
|
11
|
+
|
|
12
|
+
::: ctrader_api_client.CTraderClient
|
|
13
|
+
options:
|
|
14
|
+
show_source: false
|
|
15
|
+
members:
|
|
16
|
+
- __init__
|
|
17
|
+
- from_graph
|
|
18
|
+
- "on"
|
|
19
|
+
- "off"
|
|
20
|
+
- auth
|
|
21
|
+
- accounts
|
|
22
|
+
- symbols
|
|
23
|
+
- trading
|
|
24
|
+
- market_data
|
|
25
|
+
- is_connected
|
|
26
|
+
- protocol
|
|
27
|
+
|
|
28
|
+
## Composition
|
|
29
|
+
|
|
30
|
+
`CTraderClient(config)` assembles its own collaborators, which is all most
|
|
31
|
+
callers need. When you have to substitute one of them, assemble the graph
|
|
32
|
+
yourself and hand it over:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from ctrader_api_client import CTraderClient
|
|
36
|
+
from ctrader_api_client.composition import build_graph
|
|
37
|
+
|
|
38
|
+
graph = build_graph(config, clock=my_clock)
|
|
39
|
+
client = CTraderClient.from_graph(graph)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
::: ctrader_api_client.composition.build_graph
|
|
43
|
+
options:
|
|
44
|
+
show_source: false
|
|
45
|
+
|
|
46
|
+
::: ctrader_api_client.composition.ClientGraph
|
|
47
|
+
options:
|
|
48
|
+
show_source: false
|
|
49
|
+
|
|
50
|
+
## ClientConfig
|
|
51
|
+
|
|
52
|
+
::: ctrader_api_client.ClientConfig
|
|
53
|
+
options:
|
|
54
|
+
show_source: false
|
|
55
|
+
|
|
56
|
+
## Authentication Policies
|
|
57
|
+
|
|
58
|
+
Token-refresh and session-recovery timing, passed via
|
|
59
|
+
`ClientConfig(refresh_policy=..., reauth_policy=...)`.
|
|
60
|
+
|
|
61
|
+
::: ctrader_api_client.auth.RefreshPolicy
|
|
62
|
+
options:
|
|
63
|
+
show_source: false
|
|
64
|
+
|
|
65
|
+
::: ctrader_api_client.auth.ReauthPolicy
|
|
66
|
+
options:
|
|
67
|
+
show_source: false
|
|
68
|
+
|
|
69
|
+
## Authentication
|
|
70
|
+
|
|
71
|
+
The `client.auth` property provides access to authentication operations. The
|
|
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.
|
|
74
|
+
|
|
75
|
+
::: ctrader_api_client.auth.AuthManager
|
|
76
|
+
options:
|
|
77
|
+
show_source: false
|
|
78
|
+
members:
|
|
79
|
+
- authenticate_trader
|
|
80
|
+
- is_account_authorized
|
|
81
|
+
- get_credentials
|
|
82
|
+
- all_credentials
|
|
83
|
+
- remove_account
|
|
84
|
+
- is_app_authenticated
|
|
85
|
+
- authenticated_accounts
|
|
86
|
+
- authorized_accounts
|
|
87
|
+
|
|
88
|
+
## AccountCredentials
|
|
89
|
+
|
|
90
|
+
::: ctrader_api_client.auth.AccountCredentials
|
|
91
|
+
options:
|
|
92
|
+
show_source: false
|
|
93
|
+
|
|
94
|
+
## TokenStore
|
|
95
|
+
|
|
96
|
+
Passed as `CTraderClient(config, token_store=...)`. cTrader rotates both tokens
|
|
97
|
+
on every refresh and invalidates the old pair immediately, so a process that
|
|
98
|
+
restarts holding the pair it was originally given can no longer authenticate.
|
|
99
|
+
|
|
100
|
+
::: ctrader_api_client.auth.TokenStore
|
|
101
|
+
options:
|
|
102
|
+
show_source: false
|
|
103
|
+
members: true
|
|
104
|
+
|
|
105
|
+
The contract is write-only, because writing is the half the client has to do for
|
|
106
|
+
you: rotation happens mid-session, at a moment you cannot observe. Reading back at
|
|
107
|
+
startup is yours, since only you know which accounts a given process is
|
|
108
|
+
responsible for. Nothing stops the same class from doing both:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
class PostgresTokenStore(TokenStore):
|
|
112
|
+
async def save(self, credentials: AccountCredentials) -> None:
|
|
113
|
+
... # required by the protocol, called by the client
|
|
114
|
+
|
|
115
|
+
async def load(self, account_id: int) -> AccountCredentials | None:
|
|
116
|
+
... # not part of the protocol, called by you at startup
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
store = PostgresTokenStore(pool)
|
|
120
|
+
client = CTraderClient(config, token_store=store)
|
|
121
|
+
|
|
122
|
+
async with client:
|
|
123
|
+
stored = await store.load(account_id)
|
|
124
|
+
if stored is None:
|
|
125
|
+
stored = AccountCredentials(
|
|
126
|
+
account_id=account_id,
|
|
127
|
+
access_token="your_access_token",
|
|
128
|
+
refresh_token="your_refresh_token",
|
|
129
|
+
expires_at=1778617423,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
await client.auth.authenticate_trader(stored)
|
|
133
|
+
```
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Enumeration types used throughout the API.
|
|
4
4
|
|
|
5
5
|
```python
|
|
6
|
-
from ctrader_api_client
|
|
6
|
+
from ctrader_api_client import ExecutionType, OrderSide, OrderType
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
## Trading Enums
|
|
@@ -51,12 +51,6 @@ from ctrader_api_client.enums import OrderType, OrderSide, ExecutionType
|
|
|
51
51
|
## Account Enums
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
::: ctrader_api_client.enums.Environment
|
|
55
|
-
options:
|
|
56
|
-
show_source: false
|
|
57
|
-
members: true
|
|
58
|
-
|
|
59
|
-
|
|
60
54
|
::: ctrader_api_client.enums.AccessRights
|
|
61
55
|
options:
|
|
62
56
|
show_source: false
|