ctrader-api-client 0.1.0__py3-none-any.whl
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/__init__.py +64 -0
- ctrader_api_client/_internal/__init__.py +26 -0
- ctrader_api_client/_internal/messages.py +348 -0
- ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +42 -0
- ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +30 -0
- ctrader_api_client/_internal/proto/OpenApiMessages.py +1112 -0
- ctrader_api_client/_internal/proto/OpenApiModelMessages.py +802 -0
- ctrader_api_client/_internal/proto/__init__.py +320 -0
- ctrader_api_client/_internal/serialization.py +84 -0
- ctrader_api_client/api/__init__.py +21 -0
- ctrader_api_client/api/accounts.py +71 -0
- ctrader_api_client/api/market_data.py +424 -0
- ctrader_api_client/api/symbols.py +171 -0
- ctrader_api_client/api/trading.py +506 -0
- ctrader_api_client/auth/__init__.py +14 -0
- ctrader_api_client/auth/credentials.py +72 -0
- ctrader_api_client/auth/manager.py +511 -0
- ctrader_api_client/client.py +475 -0
- ctrader_api_client/config.py +56 -0
- ctrader_api_client/connection/__init__.py +16 -0
- ctrader_api_client/connection/heartbeat.py +120 -0
- ctrader_api_client/connection/protocol.py +366 -0
- ctrader_api_client/connection/transport.py +123 -0
- ctrader_api_client/enums.py +138 -0
- ctrader_api_client/events/__init__.py +65 -0
- ctrader_api_client/events/emitter.py +254 -0
- ctrader_api_client/events/router.py +400 -0
- ctrader_api_client/events/types.py +340 -0
- ctrader_api_client/exceptions.py +231 -0
- ctrader_api_client/models/__init__.py +50 -0
- ctrader_api_client/models/_base.py +19 -0
- ctrader_api_client/models/account.py +177 -0
- ctrader_api_client/models/deal.py +242 -0
- ctrader_api_client/models/market_data.py +192 -0
- ctrader_api_client/models/order.py +262 -0
- ctrader_api_client/models/position.py +209 -0
- ctrader_api_client/models/requests.py +299 -0
- ctrader_api_client/models/symbol.py +194 -0
- ctrader_api_client/py.typed +0 -0
- ctrader_api_client-0.1.0.dist-info/METADATA +252 -0
- ctrader_api_client-0.1.0.dist-info/RECORD +43 -0
- ctrader_api_client-0.1.0.dist-info/WHEEL +4 -0
- ctrader_api_client-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Not yet implemented:
|
|
3
|
+
- ProtoOAMarginCallUpdateEvent: Margin call threshold configuration changed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from decimal import Decimal
|
|
11
|
+
|
|
12
|
+
from ..enums import ExecutionType, OrderSide
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class SpotEvent:
|
|
17
|
+
"""Price tick event.
|
|
18
|
+
|
|
19
|
+
Emitted when bid/ask prices update for a subscribed symbol.
|
|
20
|
+
|
|
21
|
+
Note: Prices are raw integer values from the server. To convert to
|
|
22
|
+
actual prices, divide by 10^5 (e.g., 123000 = 1.23000). The exact
|
|
23
|
+
decimal places depend on symbol configuration.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
account_id: The cTID trader account ID.
|
|
27
|
+
symbol_id: The symbol identifier.
|
|
28
|
+
bid: Current bid price as raw integer, or None if unchanged.
|
|
29
|
+
ask: Current ask price as raw integer, or None if unchanged.
|
|
30
|
+
timestamp: Server timestamp of the tick.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
account_id: int
|
|
34
|
+
symbol_id: int
|
|
35
|
+
bid: int | None
|
|
36
|
+
ask: int | None
|
|
37
|
+
timestamp: datetime
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class ExecutionEvent:
|
|
42
|
+
"""Order execution event.
|
|
43
|
+
|
|
44
|
+
Emitted when an order is accepted, filled, modified, canceled, etc.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
account_id: The cTID trader account ID.
|
|
48
|
+
execution_type: Type of execution (fill, cancel, etc.).
|
|
49
|
+
order_id: The order identifier.
|
|
50
|
+
position_id: The position identifier (if applicable).
|
|
51
|
+
symbol_id: The symbol identifier.
|
|
52
|
+
side: Order side (buy/sell).
|
|
53
|
+
filled_volume: Volume filled in this execution (in cents).
|
|
54
|
+
fill_price: Execution price (if filled).
|
|
55
|
+
timestamp: Server timestamp of the execution.
|
|
56
|
+
is_server_event: True if generated by server (e.g., stop-out).
|
|
57
|
+
error_code: Error code if execution failed.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
account_id: int
|
|
61
|
+
execution_type: ExecutionType
|
|
62
|
+
order_id: int
|
|
63
|
+
position_id: int | None
|
|
64
|
+
symbol_id: int
|
|
65
|
+
side: OrderSide
|
|
66
|
+
filled_volume: int | None
|
|
67
|
+
fill_price: Decimal | None
|
|
68
|
+
timestamp: datetime
|
|
69
|
+
is_server_event: bool = False
|
|
70
|
+
error_code: str | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class OrderErrorEvent:
|
|
75
|
+
"""Order error event.
|
|
76
|
+
|
|
77
|
+
Emitted when an order operation fails.
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
account_id: The cTID trader account ID.
|
|
81
|
+
order_id: The order identifier (if available).
|
|
82
|
+
position_id: The position identifier (if available).
|
|
83
|
+
error_code: The error code from the server.
|
|
84
|
+
description: Human-readable error description.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
account_id: int
|
|
88
|
+
order_id: int | None
|
|
89
|
+
position_id: int | None
|
|
90
|
+
error_code: str
|
|
91
|
+
description: str
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True, slots=True)
|
|
95
|
+
class TraderUpdateEvent:
|
|
96
|
+
"""Trader account update event.
|
|
97
|
+
|
|
98
|
+
Emitted when account information changes (balance, leverage, etc.).
|
|
99
|
+
|
|
100
|
+
Attributes:
|
|
101
|
+
account_id: The cTID trader account ID.
|
|
102
|
+
balance: Current account balance (raw, divide by 10^moneyDigits).
|
|
103
|
+
leverage_in_cents: Account leverage in cents (5000 = 1:50).
|
|
104
|
+
money_digits: Exponent for monetary values.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
account_id: int
|
|
108
|
+
balance: int
|
|
109
|
+
leverage_in_cents: int | None
|
|
110
|
+
money_digits: int
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True, slots=True)
|
|
114
|
+
class MarginChangeEvent:
|
|
115
|
+
"""Position margin change event.
|
|
116
|
+
|
|
117
|
+
Emitted when the margin allocated to a position changes.
|
|
118
|
+
|
|
119
|
+
Attributes:
|
|
120
|
+
account_id: The cTID trader account ID.
|
|
121
|
+
position_id: The position identifier.
|
|
122
|
+
used_margin: New margin value (raw, divide by 10^moneyDigits).
|
|
123
|
+
money_digits: Exponent for monetary values.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
account_id: int
|
|
127
|
+
position_id: int
|
|
128
|
+
used_margin: int
|
|
129
|
+
money_digits: int
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True, slots=True)
|
|
133
|
+
class DepthQuote:
|
|
134
|
+
"""
|
|
135
|
+
Single depth of market quote.
|
|
136
|
+
|
|
137
|
+
Attributes:
|
|
138
|
+
quote_id: Unique identifier for the quote (used for updates/deletions).
|
|
139
|
+
price: Quote price (raw integer, divide by 10^priceDigits).
|
|
140
|
+
size: Quote size (volume in cents).
|
|
141
|
+
is_bid: True if this is a bid quote, False if it's an ask quote.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
quote_id: int
|
|
145
|
+
price: int
|
|
146
|
+
size: int
|
|
147
|
+
is_bid: bool
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True, slots=True)
|
|
151
|
+
class DepthEvent:
|
|
152
|
+
"""Market depth (order book) event.
|
|
153
|
+
|
|
154
|
+
Emitted when the order book updates for a subscribed symbol.
|
|
155
|
+
|
|
156
|
+
Attributes:
|
|
157
|
+
account_id: The cTID trader account ID.
|
|
158
|
+
symbol_id: The symbol identifier.
|
|
159
|
+
new_quotes: New or updated quotes.
|
|
160
|
+
deleted_quote_ids: IDs of quotes that were removed.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
account_id: int
|
|
164
|
+
symbol_id: int
|
|
165
|
+
new_quotes: tuple[DepthQuote, ...]
|
|
166
|
+
deleted_quote_ids: tuple[int, ...]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(frozen=True, slots=True)
|
|
170
|
+
class TokenInvalidatedEvent:
|
|
171
|
+
"""Token invalidated event.
|
|
172
|
+
|
|
173
|
+
Emitted when access tokens are revoked or expired.
|
|
174
|
+
|
|
175
|
+
Attributes:
|
|
176
|
+
account_ids: List of affected account IDs.
|
|
177
|
+
reason: Reason for invalidation.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
account_ids: tuple[int, ...]
|
|
181
|
+
reason: str
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True, slots=True)
|
|
185
|
+
class ClientDisconnectEvent:
|
|
186
|
+
"""Client disconnect event.
|
|
187
|
+
|
|
188
|
+
Emitted when the server terminates the client connection.
|
|
189
|
+
|
|
190
|
+
Attributes:
|
|
191
|
+
reason: Reason for disconnection.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
reason: str
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True, slots=True)
|
|
198
|
+
class AccountDisconnectEvent:
|
|
199
|
+
"""Account disconnect event.
|
|
200
|
+
|
|
201
|
+
Emitted when a specific account session is terminated.
|
|
202
|
+
|
|
203
|
+
Attributes:
|
|
204
|
+
account_id: The cTID trader account ID.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
account_id: int
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(frozen=True, slots=True)
|
|
211
|
+
class SymbolChangedEvent:
|
|
212
|
+
"""Symbol configuration changed event.
|
|
213
|
+
|
|
214
|
+
Emitted when a symbol's configuration is updated (trading hours,
|
|
215
|
+
margin requirements, spreads, etc.).
|
|
216
|
+
|
|
217
|
+
Attributes:
|
|
218
|
+
account_id: The cTID trader account ID.
|
|
219
|
+
symbol_ids: List of symbol IDs that changed.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
account_id: int
|
|
223
|
+
symbol_ids: tuple[int, ...]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass(frozen=True, slots=True)
|
|
227
|
+
class TrailingStopChangedEvent:
|
|
228
|
+
"""Trailing stop loss level changed event.
|
|
229
|
+
|
|
230
|
+
Emitted when a trailing stop loss price is updated due to
|
|
231
|
+
favorable price movement.
|
|
232
|
+
|
|
233
|
+
Attributes:
|
|
234
|
+
account_id: The cTID trader account ID.
|
|
235
|
+
position_id: The position identifier.
|
|
236
|
+
order_id: The stop loss order identifier.
|
|
237
|
+
stop_price: New stop loss price.
|
|
238
|
+
timestamp: Server timestamp of the update.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
account_id: int
|
|
242
|
+
position_id: int
|
|
243
|
+
order_id: int
|
|
244
|
+
stop_price: Decimal
|
|
245
|
+
timestamp: datetime
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@dataclass(frozen=True, slots=True)
|
|
249
|
+
class MarginCallTriggerEvent:
|
|
250
|
+
"""Margin call triggered event.
|
|
251
|
+
|
|
252
|
+
Emitted when account margin level reaches a configured threshold.
|
|
253
|
+
Sent at most once every 10 minutes per threshold to avoid spam.
|
|
254
|
+
|
|
255
|
+
Attributes:
|
|
256
|
+
account_id: The cTID trader account ID.
|
|
257
|
+
margin_call_type: Type of margin call (1, 2, or 3 for different thresholds).
|
|
258
|
+
margin_level_threshold: The threshold that was breached (percentage).
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
account_id: int
|
|
262
|
+
margin_call_type: int
|
|
263
|
+
margin_level_threshold: Decimal
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass(frozen=True, slots=True)
|
|
267
|
+
class PnLChangeEvent:
|
|
268
|
+
"""Unrealized PnL change event.
|
|
269
|
+
|
|
270
|
+
Emitted when unrealized profit/loss changes due to market movement.
|
|
271
|
+
Requires subscription via ProtoOAv1PnLChangeSubscribeReq.
|
|
272
|
+
|
|
273
|
+
Attributes:
|
|
274
|
+
account_id: The cTID trader account ID.
|
|
275
|
+
gross_unrealized_pnl: Gross unrealized PnL (raw, divide by 10^moneyDigits).
|
|
276
|
+
net_unrealized_pnl: Net unrealized PnL (raw, divide by 10^moneyDigits).
|
|
277
|
+
money_digits: Exponent for monetary values.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
account_id: int
|
|
281
|
+
gross_unrealized_pnl: int
|
|
282
|
+
net_unrealized_pnl: int
|
|
283
|
+
money_digits: int
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass(frozen=True, slots=True)
|
|
287
|
+
class ReconnectedEvent:
|
|
288
|
+
"""Emitted after automatic reconnection and re-authentication.
|
|
289
|
+
|
|
290
|
+
When a connection is lost and automatically restored, the client
|
|
291
|
+
re-authenticates the app and all previously authenticated accounts.
|
|
292
|
+
Subscriptions (spots, trendbars, depth) are NOT automatically restored
|
|
293
|
+
and should be handled using ReadyEvent instead.
|
|
294
|
+
Use this event for any custom logic that depends on reconnection, such as logging or alerting.
|
|
295
|
+
|
|
296
|
+
Attributes:
|
|
297
|
+
app_auth_restored: Whether app authentication succeeded.
|
|
298
|
+
restored_accounts: Account IDs that were successfully re-authenticated.
|
|
299
|
+
failed_accounts: Accounts that failed, as (account_id, error_message) tuples.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
app_auth_restored: bool
|
|
303
|
+
restored_accounts: tuple[int, ...]
|
|
304
|
+
failed_accounts: tuple[tuple[int, str], ...]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@dataclass(frozen=True, slots=True)
|
|
308
|
+
class ReadyEvent:
|
|
309
|
+
"""Emitted when an account is authenticated and ready for use.
|
|
310
|
+
|
|
311
|
+
Fired after both initial authentication and reconnection re-authentication.
|
|
312
|
+
Use this to set up subscriptions that should persist across reconnections.
|
|
313
|
+
|
|
314
|
+
Attributes:
|
|
315
|
+
account_id: The cTID trader account ID that is now ready.
|
|
316
|
+
is_reconnect: True if this follows a reconnection, False for initial auth.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
account_id: int
|
|
320
|
+
is_reconnect: bool
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# Type alias for any event type
|
|
324
|
+
type Event = (
|
|
325
|
+
SpotEvent
|
|
326
|
+
| ExecutionEvent
|
|
327
|
+
| OrderErrorEvent
|
|
328
|
+
| TraderUpdateEvent
|
|
329
|
+
| MarginChangeEvent
|
|
330
|
+
| DepthEvent
|
|
331
|
+
| TokenInvalidatedEvent
|
|
332
|
+
| ClientDisconnectEvent
|
|
333
|
+
| AccountDisconnectEvent
|
|
334
|
+
| SymbolChangedEvent
|
|
335
|
+
| TrailingStopChangedEvent
|
|
336
|
+
| MarginCallTriggerEvent
|
|
337
|
+
| PnLChangeEvent
|
|
338
|
+
| ReconnectedEvent
|
|
339
|
+
| ReadyEvent
|
|
340
|
+
)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ._internal.proto import ProtoOAErrorCode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ._internal.proto import ProtoOAErrorRes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CTraderError(Exception):
|
|
13
|
+
"""Base exception for all cTrader API errors."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Connection Errors
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CTraderConnectionError(CTraderError):
|
|
22
|
+
"""Base exception for connection-related errors."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CTraderConnectionFailedError(CTraderConnectionError):
|
|
26
|
+
"""Failed to establish connection to the server."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, host: str, port: int, cause: Exception | None = None) -> None:
|
|
29
|
+
self.host = host
|
|
30
|
+
self.port = port
|
|
31
|
+
self.cause = cause
|
|
32
|
+
message = f"Failed to connect to {host}:{port}"
|
|
33
|
+
if cause:
|
|
34
|
+
message += f": {cause}"
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CTraderConnectionClosedError(CTraderConnectionError):
|
|
39
|
+
"""Connection was closed unexpectedly."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, reason: str | None = None, was_clean: bool = False) -> None:
|
|
42
|
+
self.reason = reason
|
|
43
|
+
self.was_clean = was_clean
|
|
44
|
+
message = "Connection closed"
|
|
45
|
+
if was_clean:
|
|
46
|
+
message += " cleanly"
|
|
47
|
+
if reason:
|
|
48
|
+
message += f": {reason}"
|
|
49
|
+
super().__init__(message)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CTraderConnectionTimeoutError(CTraderConnectionError):
|
|
53
|
+
"""Connection operation timed out."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, timeout_seconds: float, operation: str = "operation") -> None:
|
|
56
|
+
self.timeout_seconds = timeout_seconds
|
|
57
|
+
self.operation = operation
|
|
58
|
+
super().__init__(f"{operation} timed out after {timeout_seconds}s")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# =============================================================================
|
|
62
|
+
# Authentication Errors
|
|
63
|
+
# =============================================================================
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AuthenticationError(CTraderError):
|
|
67
|
+
"""Base exception for authentication-related errors."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ApplicationAuthError(AuthenticationError):
|
|
71
|
+
"""Application authentication failed."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, error_code: str, description: str | None = None) -> None:
|
|
74
|
+
self.error_code = error_code
|
|
75
|
+
self.description = description
|
|
76
|
+
message = f"Application authentication failed: {error_code}"
|
|
77
|
+
if description:
|
|
78
|
+
message += f" - {description}"
|
|
79
|
+
super().__init__(message)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AccountAuthError(AuthenticationError):
|
|
83
|
+
"""Account authentication failed."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
error_code: str,
|
|
88
|
+
description: str | None = None,
|
|
89
|
+
ctid_trader_account_id: int | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
self.error_code = error_code
|
|
92
|
+
self.description = description
|
|
93
|
+
self.ctid_trader_account_id = ctid_trader_account_id
|
|
94
|
+
message = f"Account authentication failed: {error_code}"
|
|
95
|
+
if ctid_trader_account_id:
|
|
96
|
+
message += f" (account: {ctid_trader_account_id})"
|
|
97
|
+
if description:
|
|
98
|
+
message += f" - {description}"
|
|
99
|
+
super().__init__(message)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TokenExpiredError(AuthenticationError):
|
|
103
|
+
"""Access token has expired."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, ctid_trader_account_id: int | None = None) -> None:
|
|
106
|
+
self.ctid_trader_account_id = ctid_trader_account_id
|
|
107
|
+
message = "Access token has expired"
|
|
108
|
+
if ctid_trader_account_id:
|
|
109
|
+
message += f" for account {ctid_trader_account_id}"
|
|
110
|
+
super().__init__(message)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TokenRefreshError(AuthenticationError):
|
|
114
|
+
"""Failed to refresh access token."""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
ctid_trader_account_id: int | None = None,
|
|
119
|
+
cause: Exception | None = None,
|
|
120
|
+
) -> None:
|
|
121
|
+
self.ctid_trader_account_id = ctid_trader_account_id
|
|
122
|
+
self.cause = cause
|
|
123
|
+
message = "Failed to refresh access token"
|
|
124
|
+
if ctid_trader_account_id:
|
|
125
|
+
message += f" for account {ctid_trader_account_id}"
|
|
126
|
+
if cause:
|
|
127
|
+
message += f": {cause}"
|
|
128
|
+
super().__init__(message)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class AccountNotFoundError(AuthenticationError):
|
|
132
|
+
"""No account found matching the given criteria."""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
trader_login: int,
|
|
137
|
+
available_logins: list[int] | None = None,
|
|
138
|
+
) -> None:
|
|
139
|
+
self.trader_login = trader_login
|
|
140
|
+
self.available_logins = available_logins
|
|
141
|
+
message = f"No account found with trader login {trader_login}"
|
|
142
|
+
if available_logins:
|
|
143
|
+
message += f". Available logins: {available_logins}"
|
|
144
|
+
super().__init__(message)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# =============================================================================
|
|
148
|
+
# API Errors
|
|
149
|
+
# =============================================================================
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class APIError(CTraderError):
|
|
153
|
+
"""Error response from the cTrader API."""
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
error_code: str,
|
|
158
|
+
description: str | None = None,
|
|
159
|
+
ctid_trader_account_id: int | None = None,
|
|
160
|
+
maintenance_end_timestamp: int | None = None,
|
|
161
|
+
retry_after: int | None = None,
|
|
162
|
+
) -> None:
|
|
163
|
+
self.error_code = error_code
|
|
164
|
+
self.description = description
|
|
165
|
+
self.ctid_trader_account_id = ctid_trader_account_id
|
|
166
|
+
self.maintenance_end_timestamp = maintenance_end_timestamp
|
|
167
|
+
self.retry_after = retry_after
|
|
168
|
+
|
|
169
|
+
message = f"API error: {error_code}"
|
|
170
|
+
if ctid_trader_account_id:
|
|
171
|
+
message += f" (account: {ctid_trader_account_id})"
|
|
172
|
+
if description:
|
|
173
|
+
message += f" - {description}"
|
|
174
|
+
super().__init__(message)
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def from_proto(cls, error_res: ProtoOAErrorRes) -> APIError:
|
|
178
|
+
"""Create an APIError from a ProtoOAErrorRes message."""
|
|
179
|
+
return cls(
|
|
180
|
+
error_code=error_res.error_code,
|
|
181
|
+
description=error_res.description or None,
|
|
182
|
+
ctid_trader_account_id=error_res.ctid_trader_account_id or None,
|
|
183
|
+
maintenance_end_timestamp=error_res.maintenance_end_timestamp or None,
|
|
184
|
+
retry_after=error_res.retry_after or None,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def is_rate_limited(self) -> bool:
|
|
188
|
+
"""Check if this error indicates rate limiting."""
|
|
189
|
+
return self.error_code == ProtoOAErrorCode.REQUEST_FREQUENCY_EXCEEDED.name or self.retry_after is not None
|
|
190
|
+
|
|
191
|
+
def is_maintenance(self) -> bool:
|
|
192
|
+
"""Check if this error indicates server maintenance."""
|
|
193
|
+
return (
|
|
194
|
+
self.error_code == ProtoOAErrorCode.SERVER_IS_UNDER_MAINTENANCE.name
|
|
195
|
+
or self.maintenance_end_timestamp is not None
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# =============================================================================
|
|
200
|
+
# Protocol Errors
|
|
201
|
+
# =============================================================================
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ProtocolError(CTraderError):
|
|
205
|
+
"""Base exception for protocol-level errors."""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class FramingError(ProtocolError):
|
|
209
|
+
"""Error in wire protocol framing."""
|
|
210
|
+
|
|
211
|
+
def __init__(self, expected_bytes: int, received_bytes: int) -> None:
|
|
212
|
+
self.expected_bytes = expected_bytes
|
|
213
|
+
self.received_bytes = received_bytes
|
|
214
|
+
super().__init__(f"Framing error: expected {expected_bytes} bytes, received {received_bytes}")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class DeserializationError(ProtocolError):
|
|
218
|
+
"""Failed to deserialize a protobuf message."""
|
|
219
|
+
|
|
220
|
+
def __init__(self, payload_type: int, raw_data: bytes) -> None:
|
|
221
|
+
self.payload_type = payload_type
|
|
222
|
+
self.raw_data = raw_data
|
|
223
|
+
super().__init__(f"Failed to deserialize message with payload type {payload_type} ({len(raw_data)} bytes)")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class UnknownPayloadTypeError(ProtocolError):
|
|
227
|
+
"""Received a message with an unknown payload type."""
|
|
228
|
+
|
|
229
|
+
def __init__(self, payload_type: int) -> None:
|
|
230
|
+
self.payload_type = payload_type
|
|
231
|
+
super().__init__(f"Unknown payload type: {payload_type}")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""High-level Pydantic models for cTrader API.
|
|
2
|
+
|
|
3
|
+
This module provides Pythonic wrappers over raw protobuf types,
|
|
4
|
+
with conversion methods and price/volume utilities.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
```python
|
|
8
|
+
from ctrader_api_client.models import (
|
|
9
|
+
Account,
|
|
10
|
+
Symbol,
|
|
11
|
+
Position,
|
|
12
|
+
NewOrderRequest,
|
|
13
|
+
)
|
|
14
|
+
from ctrader_api_client.enums import OrderType, OrderSide
|
|
15
|
+
|
|
16
|
+
# Create an order request
|
|
17
|
+
request = NewOrderRequest(
|
|
18
|
+
symbol_id=1,
|
|
19
|
+
side=OrderSide.BUY,
|
|
20
|
+
volume=100, # 0.01 lots
|
|
21
|
+
order_type=OrderType.MARKET,
|
|
22
|
+
)
|
|
23
|
+
```
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .account import Account, AccountSummary
|
|
27
|
+
from .deal import CloseDetail, Deal
|
|
28
|
+
from .market_data import TickData, Trendbar
|
|
29
|
+
from .order import Order
|
|
30
|
+
from .position import Position
|
|
31
|
+
from .requests import AmendOrderRequest, AmendPositionRequest, ClosePositionRequest, NewOrderRequest
|
|
32
|
+
from .symbol import Symbol, SymbolInfo
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Account",
|
|
37
|
+
"AccountSummary",
|
|
38
|
+
"AmendOrderRequest",
|
|
39
|
+
"AmendPositionRequest",
|
|
40
|
+
"CloseDetail",
|
|
41
|
+
"ClosePositionRequest",
|
|
42
|
+
"Deal",
|
|
43
|
+
"NewOrderRequest",
|
|
44
|
+
"Order",
|
|
45
|
+
"Position",
|
|
46
|
+
"Symbol",
|
|
47
|
+
"SymbolInfo",
|
|
48
|
+
"TickData",
|
|
49
|
+
"Trendbar",
|
|
50
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FrozenModel(BaseModel):
|
|
7
|
+
"""Base class for all immutable models.
|
|
8
|
+
|
|
9
|
+
All models derived from this class are:
|
|
10
|
+
- Frozen (immutable after creation)
|
|
11
|
+
- Strict (no type coercion)
|
|
12
|
+
- Forbid extra fields
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(
|
|
16
|
+
frozen=True,
|
|
17
|
+
strict=True,
|
|
18
|
+
extra="forbid",
|
|
19
|
+
)
|