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,506 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .._internal.proto import (
|
|
8
|
+
ProtoOACancelOrderReq,
|
|
9
|
+
ProtoOADealListByPositionIdReq,
|
|
10
|
+
ProtoOADealListByPositionIdRes,
|
|
11
|
+
ProtoOADealListReq,
|
|
12
|
+
ProtoOADealListRes,
|
|
13
|
+
ProtoOAExecutionEvent,
|
|
14
|
+
ProtoOAExecutionType,
|
|
15
|
+
ProtoOAOrderErrorEvent,
|
|
16
|
+
ProtoOAOrderListReq,
|
|
17
|
+
ProtoOAOrderListRes,
|
|
18
|
+
ProtoOAReconcileReq,
|
|
19
|
+
ProtoOAReconcileRes,
|
|
20
|
+
)
|
|
21
|
+
from ..enums import ExecutionType, OrderSide
|
|
22
|
+
from ..events import ExecutionEvent
|
|
23
|
+
from ..exceptions import APIError
|
|
24
|
+
from ..models import Deal, Order, Position
|
|
25
|
+
from ..models.requests import (
|
|
26
|
+
AmendOrderRequest,
|
|
27
|
+
AmendPositionRequest,
|
|
28
|
+
ClosePositionRequest,
|
|
29
|
+
NewOrderRequest,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from ..connection import Protocol
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _raise_if_order_error(response: object) -> None:
|
|
38
|
+
"""Raise APIError if response is an order error event.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
response: The response to check.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
APIError: If response is a ProtoOAOrderErrorEvent.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(response, ProtoOAOrderErrorEvent):
|
|
47
|
+
raise APIError(
|
|
48
|
+
error_code=response.error_code or "ORDER_ERROR",
|
|
49
|
+
description=response.description or None,
|
|
50
|
+
ctid_trader_account_id=response.ctid_trader_account_id or None,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Map ProtoOAExecutionType enum values to our ExecutionType
|
|
55
|
+
_EXECUTION_TYPE_MAP: dict[int, ExecutionType] = {
|
|
56
|
+
ProtoOAExecutionType.ORDER_ACCEPTED: ExecutionType.ORDER_ACCEPTED,
|
|
57
|
+
ProtoOAExecutionType.ORDER_FILLED: ExecutionType.ORDER_FILLED,
|
|
58
|
+
ProtoOAExecutionType.ORDER_REPLACED: ExecutionType.ORDER_REPLACED,
|
|
59
|
+
ProtoOAExecutionType.ORDER_CANCELLED: ExecutionType.ORDER_CANCELLED,
|
|
60
|
+
ProtoOAExecutionType.ORDER_EXPIRED: ExecutionType.ORDER_EXPIRED,
|
|
61
|
+
ProtoOAExecutionType.ORDER_REJECTED: ExecutionType.ORDER_REJECTED,
|
|
62
|
+
ProtoOAExecutionType.ORDER_CANCEL_REJECTED: ExecutionType.ORDER_CANCEL_REJECTED,
|
|
63
|
+
ProtoOAExecutionType.SWAP: ExecutionType.SWAP,
|
|
64
|
+
ProtoOAExecutionType.DEPOSIT_WITHDRAW: ExecutionType.DEPOSIT_WITHDRAW,
|
|
65
|
+
ProtoOAExecutionType.ORDER_PARTIAL_FILL: ExecutionType.ORDER_PARTIAL_FILL,
|
|
66
|
+
ProtoOAExecutionType.BONUS_DEPOSIT_WITHDRAW: ExecutionType.BONUS_DEPOSIT_WITHDRAW,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _proto_to_execution_event(proto: ProtoOAExecutionEvent) -> ExecutionEvent:
|
|
71
|
+
"""Convert ProtoOAExecutionEvent to ExecutionEvent.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
proto: The proto message to convert.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
ExecutionEvent instance.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
APIError: If execution type is unknown.
|
|
81
|
+
"""
|
|
82
|
+
exec_type = _EXECUTION_TYPE_MAP.get(proto.execution_type)
|
|
83
|
+
if exec_type is None:
|
|
84
|
+
raise APIError(
|
|
85
|
+
error_code="UNKNOWN_EXECUTION_TYPE",
|
|
86
|
+
description=f"Unknown execution type: {proto.execution_type}",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Map order side
|
|
90
|
+
side = OrderSide.BUY
|
|
91
|
+
if proto.order and proto.order.trade_data:
|
|
92
|
+
if proto.order.trade_data.trade_side == 2:
|
|
93
|
+
side = OrderSide.SELL
|
|
94
|
+
|
|
95
|
+
# Extract order details
|
|
96
|
+
order_id = proto.order.order_id if proto.order else 0
|
|
97
|
+
position_id = proto.position.position_id if proto.position else None
|
|
98
|
+
symbol_id = proto.order.trade_data.symbol_id if proto.order and proto.order.trade_data else 0
|
|
99
|
+
|
|
100
|
+
# Extract deal details
|
|
101
|
+
filled_volume = None
|
|
102
|
+
fill_price = None
|
|
103
|
+
timestamp = datetime.now(UTC)
|
|
104
|
+
|
|
105
|
+
if proto.deal:
|
|
106
|
+
filled_volume = proto.deal.filled_volume if proto.deal.filled_volume else None
|
|
107
|
+
if proto.deal.execution_price:
|
|
108
|
+
fill_price = Decimal(str(proto.deal.execution_price))
|
|
109
|
+
if proto.deal.execution_timestamp:
|
|
110
|
+
timestamp = datetime.fromtimestamp(proto.deal.execution_timestamp / 1000, tz=UTC)
|
|
111
|
+
|
|
112
|
+
return ExecutionEvent(
|
|
113
|
+
account_id=proto.ctid_trader_account_id,
|
|
114
|
+
execution_type=exec_type,
|
|
115
|
+
order_id=order_id,
|
|
116
|
+
position_id=position_id,
|
|
117
|
+
symbol_id=symbol_id,
|
|
118
|
+
side=side,
|
|
119
|
+
filled_volume=filled_volume,
|
|
120
|
+
fill_price=fill_price,
|
|
121
|
+
timestamp=timestamp,
|
|
122
|
+
is_server_event=proto.is_server_event if proto.is_server_event else False,
|
|
123
|
+
error_code=proto.error_code if proto.error_code else None,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TradingAPI:
|
|
128
|
+
"""Trading operations: orders and positions.
|
|
129
|
+
|
|
130
|
+
Provides methods for order placement, modification, cancellation,
|
|
131
|
+
and position management.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
```python
|
|
135
|
+
from ctrader_api_client.models import NewOrderRequest
|
|
136
|
+
from ctrader_api_client.enums import OrderSide, OrderType
|
|
137
|
+
|
|
138
|
+
# Place a market order
|
|
139
|
+
request = NewOrderRequest(
|
|
140
|
+
symbol_id=270,
|
|
141
|
+
side=OrderSide.BUY,
|
|
142
|
+
volume=100, # 0.01 lots
|
|
143
|
+
order_type=OrderType.MARKET,
|
|
144
|
+
)
|
|
145
|
+
execution = await client.trading.place_order(account_id, request)
|
|
146
|
+
print(f"Order {execution.order_id}: {execution.execution_type}")
|
|
147
|
+
|
|
148
|
+
# Get open positions
|
|
149
|
+
positions = await client.trading.get_open_positions(account_id)
|
|
150
|
+
for pos in positions:
|
|
151
|
+
print(f"Position {pos.position_id}: {pos.volume} @ {pos.entry_price}")
|
|
152
|
+
```
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, protocol: Protocol, default_timeout: float = 30.0) -> None:
|
|
156
|
+
"""Initialize the trading API.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
protocol: The protocol instance for sending requests.
|
|
160
|
+
default_timeout: Default request timeout in seconds.
|
|
161
|
+
"""
|
|
162
|
+
self._protocol = protocol
|
|
163
|
+
self._default_timeout = default_timeout
|
|
164
|
+
|
|
165
|
+
async def place_order(
|
|
166
|
+
self,
|
|
167
|
+
account_id: int,
|
|
168
|
+
request: NewOrderRequest,
|
|
169
|
+
timeout: float | None = None,
|
|
170
|
+
) -> ExecutionEvent:
|
|
171
|
+
"""Place a new order.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
account_id: The cTID trader account ID.
|
|
175
|
+
request: Order parameters.
|
|
176
|
+
timeout: Request timeout (uses default if None).
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
ExecutionEvent with order details.
|
|
180
|
+
|
|
181
|
+
Note:
|
|
182
|
+
For market orders, the order may be immediately filled.
|
|
183
|
+
Check execution_type to determine the outcome:
|
|
184
|
+
- ORDER_ACCEPTED: Pending order created
|
|
185
|
+
- ORDER_FILLED: Order fully executed
|
|
186
|
+
- ORDER_PARTIAL_FILL: Order partially executed
|
|
187
|
+
- ORDER_REJECTED: Order was rejected
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
APIError: If request fails.
|
|
191
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
192
|
+
"""
|
|
193
|
+
proto_request = request.to_proto(account_id)
|
|
194
|
+
|
|
195
|
+
response = await self._protocol.send_request(
|
|
196
|
+
proto_request,
|
|
197
|
+
timeout=timeout or self._default_timeout,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
_raise_if_order_error(response)
|
|
201
|
+
|
|
202
|
+
if not isinstance(response, ProtoOAExecutionEvent):
|
|
203
|
+
raise APIError(
|
|
204
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
205
|
+
description=f"Expected ProtoOAExecutionEvent, got {type(response).__name__}",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return _proto_to_execution_event(response)
|
|
209
|
+
|
|
210
|
+
async def amend_order(
|
|
211
|
+
self,
|
|
212
|
+
account_id: int,
|
|
213
|
+
request: AmendOrderRequest,
|
|
214
|
+
timeout: float | None = None,
|
|
215
|
+
) -> ExecutionEvent:
|
|
216
|
+
"""Modify a pending order.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
account_id: The cTID trader account ID.
|
|
220
|
+
request: Amendment parameters (order_id required).
|
|
221
|
+
timeout: Request timeout (uses default if None).
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
ExecutionEvent confirming the amendment.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
APIError: If request fails or order not found.
|
|
228
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
229
|
+
"""
|
|
230
|
+
proto_request = request.to_proto(account_id)
|
|
231
|
+
|
|
232
|
+
response = await self._protocol.send_request(
|
|
233
|
+
proto_request,
|
|
234
|
+
timeout=timeout or self._default_timeout,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
_raise_if_order_error(response)
|
|
238
|
+
|
|
239
|
+
if not isinstance(response, ProtoOAExecutionEvent):
|
|
240
|
+
raise APIError(
|
|
241
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
242
|
+
description=f"Expected ProtoOAExecutionEvent, got {type(response).__name__}",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return _proto_to_execution_event(response)
|
|
246
|
+
|
|
247
|
+
async def cancel_order(
|
|
248
|
+
self,
|
|
249
|
+
account_id: int,
|
|
250
|
+
order_id: int,
|
|
251
|
+
timeout: float | None = None,
|
|
252
|
+
) -> ExecutionEvent:
|
|
253
|
+
"""Cancel a pending order.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
account_id: The cTID trader account ID.
|
|
257
|
+
order_id: The order to cancel.
|
|
258
|
+
timeout: Request timeout (uses default if None).
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
ExecutionEvent confirming cancellation.
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
APIError: If request fails or order not found.
|
|
265
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
266
|
+
"""
|
|
267
|
+
request = ProtoOACancelOrderReq(
|
|
268
|
+
ctid_trader_account_id=account_id,
|
|
269
|
+
order_id=order_id,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
response = await self._protocol.send_request(
|
|
273
|
+
request,
|
|
274
|
+
timeout=timeout or self._default_timeout,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
_raise_if_order_error(response)
|
|
278
|
+
|
|
279
|
+
if not isinstance(response, ProtoOAExecutionEvent):
|
|
280
|
+
raise APIError(
|
|
281
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
282
|
+
description=f"Expected ProtoOAExecutionEvent, got {type(response).__name__}",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return _proto_to_execution_event(response)
|
|
286
|
+
|
|
287
|
+
async def close_position(
|
|
288
|
+
self,
|
|
289
|
+
account_id: int,
|
|
290
|
+
request: ClosePositionRequest,
|
|
291
|
+
timeout: float | None = None,
|
|
292
|
+
) -> ExecutionEvent:
|
|
293
|
+
"""Close a position (fully or partially).
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
account_id: The cTID trader account ID.
|
|
297
|
+
request: Close parameters (position_id and volume required).
|
|
298
|
+
timeout: Request timeout (uses default if None).
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
ExecutionEvent with closing details.
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
APIError: If request fails or position not found.
|
|
305
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
306
|
+
"""
|
|
307
|
+
proto_request = request.to_proto(account_id)
|
|
308
|
+
|
|
309
|
+
response = await self._protocol.send_request(
|
|
310
|
+
proto_request,
|
|
311
|
+
timeout=timeout or self._default_timeout,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
_raise_if_order_error(response)
|
|
315
|
+
|
|
316
|
+
if not isinstance(response, ProtoOAExecutionEvent):
|
|
317
|
+
raise APIError(
|
|
318
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
319
|
+
description=f"Expected ProtoOAExecutionEvent, got {type(response).__name__}",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return _proto_to_execution_event(response)
|
|
323
|
+
|
|
324
|
+
async def amend_position(
|
|
325
|
+
self,
|
|
326
|
+
account_id: int,
|
|
327
|
+
request: AmendPositionRequest,
|
|
328
|
+
timeout: float | None = None,
|
|
329
|
+
) -> ExecutionEvent:
|
|
330
|
+
"""Modify position stop loss and take profit.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
account_id: The cTID trader account ID.
|
|
334
|
+
request: Amendment parameters (position_id required).
|
|
335
|
+
timeout: Request timeout (uses default if None).
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
ExecutionEvent confirming the amendment.
|
|
339
|
+
|
|
340
|
+
Raises:
|
|
341
|
+
APIError: If request fails or position not found.
|
|
342
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
343
|
+
"""
|
|
344
|
+
proto_request = request.to_proto(account_id)
|
|
345
|
+
|
|
346
|
+
response = await self._protocol.send_request(
|
|
347
|
+
proto_request,
|
|
348
|
+
timeout=timeout or self._default_timeout,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
_raise_if_order_error(response)
|
|
352
|
+
|
|
353
|
+
if not isinstance(response, ProtoOAExecutionEvent):
|
|
354
|
+
raise APIError(
|
|
355
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
356
|
+
description=f"Expected ProtoOAExecutionEvent, got {type(response).__name__}",
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return _proto_to_execution_event(response)
|
|
360
|
+
|
|
361
|
+
async def get_open_positions(
|
|
362
|
+
self,
|
|
363
|
+
account_id: int,
|
|
364
|
+
timeout: float | None = None,
|
|
365
|
+
) -> list[Position]:
|
|
366
|
+
"""Get all open positions.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
account_id: The cTID trader account ID.
|
|
370
|
+
timeout: Request timeout (uses default if None).
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
List of open Position objects.
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
APIError: If request fails.
|
|
377
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
378
|
+
"""
|
|
379
|
+
request = ProtoOAReconcileReq(ctid_trader_account_id=account_id)
|
|
380
|
+
|
|
381
|
+
response = await self._protocol.send_request(
|
|
382
|
+
request,
|
|
383
|
+
timeout=timeout or self._default_timeout,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if not isinstance(response, ProtoOAReconcileRes):
|
|
387
|
+
raise APIError(
|
|
388
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
389
|
+
description=f"Expected ProtoOAReconcileRes, got {type(response).__name__}",
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return [Position.from_proto(p) for p in response.position]
|
|
393
|
+
|
|
394
|
+
async def get_pending_orders(
|
|
395
|
+
self,
|
|
396
|
+
account_id: int,
|
|
397
|
+
timeout: float | None = None,
|
|
398
|
+
) -> list[Order]:
|
|
399
|
+
"""Get all pending orders.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
account_id: The cTID trader account ID.
|
|
403
|
+
timeout: Request timeout (uses default if None).
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
List of pending Order objects.
|
|
407
|
+
|
|
408
|
+
Raises:
|
|
409
|
+
APIError: If request fails.
|
|
410
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
411
|
+
"""
|
|
412
|
+
request = ProtoOAOrderListReq(ctid_trader_account_id=account_id)
|
|
413
|
+
|
|
414
|
+
response = await self._protocol.send_request(
|
|
415
|
+
request,
|
|
416
|
+
timeout=timeout or self._default_timeout,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
if not isinstance(response, ProtoOAOrderListRes):
|
|
420
|
+
raise APIError(
|
|
421
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
422
|
+
description=f"Expected ProtoOAOrderListRes, got {type(response).__name__}",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
return [Order.from_proto(o) for o in response.order]
|
|
426
|
+
|
|
427
|
+
async def get_deals_by_position_id(
|
|
428
|
+
self,
|
|
429
|
+
account_id: int,
|
|
430
|
+
position_id: int,
|
|
431
|
+
) -> list[Deal]:
|
|
432
|
+
"""Get all deals for a specific position.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
account_id: The cTID trader account ID.
|
|
436
|
+
position_id: The position ID to filter deals by.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
List of Deal objects associated with the position.
|
|
440
|
+
|
|
441
|
+
Raises:
|
|
442
|
+
APIError: If request fails.
|
|
443
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
444
|
+
"""
|
|
445
|
+
request = ProtoOADealListByPositionIdReq(
|
|
446
|
+
ctid_trader_account_id=account_id,
|
|
447
|
+
position_id=position_id,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
response = await self._protocol.send_request(
|
|
451
|
+
request,
|
|
452
|
+
timeout=self._default_timeout,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if not isinstance(response, ProtoOADealListByPositionIdRes):
|
|
456
|
+
raise APIError(
|
|
457
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
458
|
+
description=f"Expected ProtoOADealListRes, got {type(response).__name__}",
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
return [Deal.from_proto(d) for d in response.deal]
|
|
462
|
+
|
|
463
|
+
async def get_deals(
|
|
464
|
+
self,
|
|
465
|
+
account_id: int,
|
|
466
|
+
from_timestamp: datetime,
|
|
467
|
+
to_timestamp: datetime,
|
|
468
|
+
timeout: float | None = None,
|
|
469
|
+
) -> list[Deal]:
|
|
470
|
+
"""Get historical deals.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
account_id: The cTID trader account ID.
|
|
474
|
+
from_timestamp: Start of time range (inclusive).
|
|
475
|
+
to_timestamp: End of time range (inclusive).
|
|
476
|
+
timeout: Request timeout (uses default if None).
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
List of Deal objects in the time range.
|
|
480
|
+
|
|
481
|
+
Note:
|
|
482
|
+
The maximum time range may be limited by the server.
|
|
483
|
+
For large ranges, consider paginating with smaller windows.
|
|
484
|
+
|
|
485
|
+
Raises:
|
|
486
|
+
APIError: If request fails.
|
|
487
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
488
|
+
"""
|
|
489
|
+
request = ProtoOADealListReq(
|
|
490
|
+
ctid_trader_account_id=account_id,
|
|
491
|
+
from_timestamp=int(from_timestamp.timestamp() * 1000),
|
|
492
|
+
to_timestamp=int(to_timestamp.timestamp() * 1000),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
response = await self._protocol.send_request(
|
|
496
|
+
request,
|
|
497
|
+
timeout=timeout or self._default_timeout,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
if not isinstance(response, ProtoOADealListRes):
|
|
501
|
+
raise APIError(
|
|
502
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
503
|
+
description=f"Expected ProtoOADealListRes, got {type(response).__name__}",
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
return [Deal.from_proto(d) for d in response.deal]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Authentication layer for cTrader API.
|
|
2
|
+
|
|
3
|
+
This module provides application and account authentication,
|
|
4
|
+
with automatic token refresh management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .credentials import AccountCredentials
|
|
8
|
+
from .manager import AuthManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AccountCredentials",
|
|
13
|
+
"AuthManager",
|
|
14
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class AccountCredentials:
|
|
9
|
+
"""Stores authentication credentials for a trading account.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
account_id: The cTID trader account ID.
|
|
13
|
+
access_token: OAuth access token for API authentication.
|
|
14
|
+
refresh_token: OAuth refresh token for obtaining new access tokens.
|
|
15
|
+
expires_at: Unix epoch timestamp when the access token expires.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
account_id: int
|
|
19
|
+
access_token: str
|
|
20
|
+
refresh_token: str
|
|
21
|
+
expires_at: float
|
|
22
|
+
|
|
23
|
+
def expires_soon(self, buffer_seconds: float = 300.0) -> bool:
|
|
24
|
+
"""Check if the access token expires within the buffer period.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
buffer_seconds: Number of seconds before expiry to consider "soon".
|
|
28
|
+
Defaults to 300 (5 minutes).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if the token expires within buffer_seconds from now.
|
|
32
|
+
"""
|
|
33
|
+
return time.time() >= (self.expires_at - buffer_seconds)
|
|
34
|
+
|
|
35
|
+
def is_expired(self) -> bool:
|
|
36
|
+
"""Check if the access token has already expired.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
True if the token has expired.
|
|
40
|
+
"""
|
|
41
|
+
return time.time() >= self.expires_at
|
|
42
|
+
|
|
43
|
+
def time_until_expiry(self) -> float:
|
|
44
|
+
"""Get seconds remaining until token expires.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Seconds until expiry. Negative if already expired.
|
|
48
|
+
"""
|
|
49
|
+
return self.expires_at - time.time()
|
|
50
|
+
|
|
51
|
+
def with_refreshed_tokens(
|
|
52
|
+
self,
|
|
53
|
+
access_token: str,
|
|
54
|
+
refresh_token: str,
|
|
55
|
+
expires_in: int,
|
|
56
|
+
) -> AccountCredentials:
|
|
57
|
+
"""Create a new instance with refreshed tokens.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
access_token: The new access token.
|
|
61
|
+
refresh_token: The new refresh token.
|
|
62
|
+
expires_in: Seconds until the new access token expires.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A new AccountCredentials instance with updated tokens.
|
|
66
|
+
"""
|
|
67
|
+
return AccountCredentials(
|
|
68
|
+
account_id=self.account_id,
|
|
69
|
+
access_token=access_token,
|
|
70
|
+
refresh_token=refresh_token,
|
|
71
|
+
expires_at=time.time() + expires_in,
|
|
72
|
+
)
|