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,424 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from .._internal.proto import (
|
|
7
|
+
ProtoOAGetTickDataReq,
|
|
8
|
+
ProtoOAGetTickDataRes,
|
|
9
|
+
ProtoOAGetTrendbarsReq,
|
|
10
|
+
ProtoOAGetTrendbarsRes,
|
|
11
|
+
ProtoOAQuoteType,
|
|
12
|
+
ProtoOASubscribeDepthQuotesReq,
|
|
13
|
+
ProtoOASubscribeDepthQuotesRes,
|
|
14
|
+
ProtoOASubscribeLiveTrendbarReq,
|
|
15
|
+
ProtoOASubscribeLiveTrendbarRes,
|
|
16
|
+
ProtoOASubscribeSpotsReq,
|
|
17
|
+
ProtoOASubscribeSpotsRes,
|
|
18
|
+
ProtoOATrendbarPeriod,
|
|
19
|
+
ProtoOAUnsubscribeDepthQuotesReq,
|
|
20
|
+
ProtoOAUnsubscribeDepthQuotesRes,
|
|
21
|
+
ProtoOAUnsubscribeLiveTrendbarReq,
|
|
22
|
+
ProtoOAUnsubscribeLiveTrendbarRes,
|
|
23
|
+
ProtoOAUnsubscribeSpotsReq,
|
|
24
|
+
ProtoOAUnsubscribeSpotsRes,
|
|
25
|
+
)
|
|
26
|
+
from ..enums import TrendbarPeriod
|
|
27
|
+
from ..exceptions import APIError
|
|
28
|
+
from ..models import TickData, Trendbar
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from ..connection import Protocol
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Map TrendbarPeriod enum to proto values
|
|
36
|
+
_PERIOD_TO_PROTO: dict[TrendbarPeriod, int] = {
|
|
37
|
+
TrendbarPeriod.M1: ProtoOATrendbarPeriod.M1,
|
|
38
|
+
TrendbarPeriod.M2: ProtoOATrendbarPeriod.M2,
|
|
39
|
+
TrendbarPeriod.M3: ProtoOATrendbarPeriod.M3,
|
|
40
|
+
TrendbarPeriod.M4: ProtoOATrendbarPeriod.M4,
|
|
41
|
+
TrendbarPeriod.M5: ProtoOATrendbarPeriod.M5,
|
|
42
|
+
TrendbarPeriod.M10: ProtoOATrendbarPeriod.M10,
|
|
43
|
+
TrendbarPeriod.M15: ProtoOATrendbarPeriod.M15,
|
|
44
|
+
TrendbarPeriod.M30: ProtoOATrendbarPeriod.M30,
|
|
45
|
+
TrendbarPeriod.H1: ProtoOATrendbarPeriod.H1,
|
|
46
|
+
TrendbarPeriod.H4: ProtoOATrendbarPeriod.H4,
|
|
47
|
+
TrendbarPeriod.H12: ProtoOATrendbarPeriod.H12,
|
|
48
|
+
TrendbarPeriod.D1: ProtoOATrendbarPeriod.D1,
|
|
49
|
+
TrendbarPeriod.W1: ProtoOATrendbarPeriod.W1,
|
|
50
|
+
TrendbarPeriod.MN1: ProtoOATrendbarPeriod.MN1,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MarketDataAPI:
|
|
55
|
+
"""Market data subscriptions and historical data.
|
|
56
|
+
|
|
57
|
+
Provides methods to subscribe to real-time market data (spots, trendbars,
|
|
58
|
+
depth of market) and retrieve historical data.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
```python
|
|
62
|
+
# Subscribe to spot prices
|
|
63
|
+
await client.market_data.subscribe_spots(account_id, [270, 271])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Handle spot events via decorator
|
|
67
|
+
@client.on(SpotEvent, symbol_id=270)
|
|
68
|
+
async def on_eurusd(event: SpotEvent) -> None:
|
|
69
|
+
print(f"EURUSD: {event.bid}/{event.ask}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Get historical candles
|
|
73
|
+
trendbars = await client.market_data.get_trendbars(
|
|
74
|
+
account_id,
|
|
75
|
+
symbol_id=270,
|
|
76
|
+
period=TrendbarPeriod.H1,
|
|
77
|
+
from_timestamp=start,
|
|
78
|
+
to_timestamp=end,
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, protocol: Protocol, default_timeout: float = 30.0) -> None:
|
|
84
|
+
"""Initialize the market data API.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
protocol: The protocol instance for sending requests.
|
|
88
|
+
default_timeout: Default request timeout in seconds.
|
|
89
|
+
"""
|
|
90
|
+
self._protocol = protocol
|
|
91
|
+
self._default_timeout = default_timeout
|
|
92
|
+
|
|
93
|
+
# -------------------------------------------------------------------------
|
|
94
|
+
# Spot Subscriptions
|
|
95
|
+
# -------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
async def subscribe_spots(
|
|
98
|
+
self,
|
|
99
|
+
account_id: int,
|
|
100
|
+
symbol_ids: list[int],
|
|
101
|
+
timeout: float | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Subscribe to spot price updates.
|
|
104
|
+
|
|
105
|
+
After subscribing, spot events will be delivered via the event system.
|
|
106
|
+
Use `@client.on(SpotEvent)` to handle them.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
account_id: The cTID trader account ID.
|
|
110
|
+
symbol_ids: Symbols to subscribe to.
|
|
111
|
+
timeout: Request timeout (uses default if None).
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
APIError: If request fails.
|
|
115
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
116
|
+
"""
|
|
117
|
+
request = ProtoOASubscribeSpotsReq(
|
|
118
|
+
ctid_trader_account_id=account_id,
|
|
119
|
+
symbol_id=symbol_ids,
|
|
120
|
+
subscribe_to_spot_timestamp=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
response = await self._protocol.send_request(
|
|
124
|
+
request,
|
|
125
|
+
timeout=timeout or self._default_timeout,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if not isinstance(response, ProtoOASubscribeSpotsRes):
|
|
129
|
+
raise APIError(
|
|
130
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
131
|
+
description=f"Expected ProtoOASubscribeSpotsRes, got {type(response).__name__}",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
async def unsubscribe_spots(
|
|
135
|
+
self,
|
|
136
|
+
account_id: int,
|
|
137
|
+
symbol_ids: list[int],
|
|
138
|
+
timeout: float | None = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Unsubscribe from spot price updates.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
account_id: The cTID trader account ID.
|
|
144
|
+
symbol_ids: Symbols to unsubscribe from.
|
|
145
|
+
timeout: Request timeout (uses default if None).
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
APIError: If request fails.
|
|
149
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
150
|
+
"""
|
|
151
|
+
request = ProtoOAUnsubscribeSpotsReq(
|
|
152
|
+
ctid_trader_account_id=account_id,
|
|
153
|
+
symbol_id=symbol_ids,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
response = await self._protocol.send_request(
|
|
157
|
+
request,
|
|
158
|
+
timeout=timeout or self._default_timeout,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if not isinstance(response, ProtoOAUnsubscribeSpotsRes):
|
|
162
|
+
raise APIError(
|
|
163
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
164
|
+
description=f"Expected ProtoOAUnsubscribeSpotsRes, got {type(response).__name__}",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# -------------------------------------------------------------------------
|
|
168
|
+
# Trendbar Subscriptions
|
|
169
|
+
# -------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
async def subscribe_trendbars(
|
|
172
|
+
self,
|
|
173
|
+
account_id: int,
|
|
174
|
+
symbol_id: int,
|
|
175
|
+
period: TrendbarPeriod,
|
|
176
|
+
timeout: float | None = None,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Subscribe to live trendbar (candle) updates.
|
|
179
|
+
|
|
180
|
+
After subscribing, trendbar events will be delivered via the event system.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
account_id: The cTID trader account ID.
|
|
184
|
+
symbol_id: Symbol to subscribe to.
|
|
185
|
+
period: Trendbar period (M1, H1, D1, etc.).
|
|
186
|
+
timeout: Request timeout (uses default if None).
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
APIError: If request fails.
|
|
190
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
191
|
+
"""
|
|
192
|
+
request = ProtoOASubscribeLiveTrendbarReq(
|
|
193
|
+
ctid_trader_account_id=account_id,
|
|
194
|
+
symbol_id=symbol_id,
|
|
195
|
+
period=ProtoOATrendbarPeriod(_PERIOD_TO_PROTO[period]),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
response = await self._protocol.send_request(
|
|
199
|
+
request,
|
|
200
|
+
timeout=timeout or self._default_timeout,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if not isinstance(response, ProtoOASubscribeLiveTrendbarRes):
|
|
204
|
+
raise APIError(
|
|
205
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
206
|
+
description=f"Expected ProtoOASubscribeLiveTrendbarRes, got {type(response).__name__}",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async def unsubscribe_trendbars(
|
|
210
|
+
self,
|
|
211
|
+
account_id: int,
|
|
212
|
+
symbol_id: int,
|
|
213
|
+
period: TrendbarPeriod,
|
|
214
|
+
timeout: float | None = None,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Unsubscribe from live trendbar updates.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
account_id: The cTID trader account ID.
|
|
220
|
+
symbol_id: Symbol to unsubscribe from.
|
|
221
|
+
period: Trendbar period.
|
|
222
|
+
timeout: Request timeout (uses default if None).
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
APIError: If request fails.
|
|
226
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
227
|
+
"""
|
|
228
|
+
request = ProtoOAUnsubscribeLiveTrendbarReq(
|
|
229
|
+
ctid_trader_account_id=account_id,
|
|
230
|
+
symbol_id=symbol_id,
|
|
231
|
+
period=ProtoOATrendbarPeriod(_PERIOD_TO_PROTO[period]),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
response = await self._protocol.send_request(
|
|
235
|
+
request,
|
|
236
|
+
timeout=timeout or self._default_timeout,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if not isinstance(response, ProtoOAUnsubscribeLiveTrendbarRes):
|
|
240
|
+
raise APIError(
|
|
241
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
242
|
+
description=f"Expected ProtoOAUnsubscribeLiveTrendbarRes, got {type(response).__name__}",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# -------------------------------------------------------------------------
|
|
246
|
+
# Depth Subscriptions
|
|
247
|
+
# -------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
async def subscribe_depth(
|
|
250
|
+
self,
|
|
251
|
+
account_id: int,
|
|
252
|
+
symbol_ids: list[int],
|
|
253
|
+
timeout: float | None = None,
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Subscribe to depth of market (order book) updates.
|
|
256
|
+
|
|
257
|
+
After subscribing, depth events will be delivered via the event system.
|
|
258
|
+
Use `@client.on(DepthEvent)` to handle them.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
account_id: The cTID trader account ID.
|
|
262
|
+
symbol_ids: Symbols to subscribe to.
|
|
263
|
+
timeout: Request timeout (uses default if None).
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
APIError: If request fails.
|
|
267
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
268
|
+
"""
|
|
269
|
+
request = ProtoOASubscribeDepthQuotesReq(
|
|
270
|
+
ctid_trader_account_id=account_id,
|
|
271
|
+
symbol_id=symbol_ids,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
response = await self._protocol.send_request(
|
|
275
|
+
request,
|
|
276
|
+
timeout=timeout or self._default_timeout,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if not isinstance(response, ProtoOASubscribeDepthQuotesRes):
|
|
280
|
+
raise APIError(
|
|
281
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
282
|
+
description=f"Expected ProtoOASubscribeDepthQuotesRes, got {type(response).__name__}",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
async def unsubscribe_depth(
|
|
286
|
+
self,
|
|
287
|
+
account_id: int,
|
|
288
|
+
symbol_ids: list[int],
|
|
289
|
+
timeout: float | None = None,
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Unsubscribe from depth of market updates.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
account_id: The cTID trader account ID.
|
|
295
|
+
symbol_ids: Symbols to unsubscribe from.
|
|
296
|
+
timeout: Request timeout (uses default if None).
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
APIError: If request fails.
|
|
300
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
301
|
+
"""
|
|
302
|
+
request = ProtoOAUnsubscribeDepthQuotesReq(
|
|
303
|
+
ctid_trader_account_id=account_id,
|
|
304
|
+
symbol_id=symbol_ids,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
response = await self._protocol.send_request(
|
|
308
|
+
request,
|
|
309
|
+
timeout=timeout or self._default_timeout,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if not isinstance(response, ProtoOAUnsubscribeDepthQuotesRes):
|
|
313
|
+
raise APIError(
|
|
314
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
315
|
+
description=f"Expected ProtoOAUnsubscribeDepthQuotesRes, got {type(response).__name__}",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# -------------------------------------------------------------------------
|
|
319
|
+
# Historical Data
|
|
320
|
+
# -------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
async def get_trendbars(
|
|
323
|
+
self,
|
|
324
|
+
account_id: int,
|
|
325
|
+
symbol_id: int,
|
|
326
|
+
period: TrendbarPeriod,
|
|
327
|
+
from_timestamp: datetime,
|
|
328
|
+
to_timestamp: datetime,
|
|
329
|
+
timeout: float | None = None,
|
|
330
|
+
) -> list[Trendbar]:
|
|
331
|
+
"""Get historical trendbars (candles).
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
account_id: The cTID trader account ID.
|
|
335
|
+
symbol_id: Symbol to get data for.
|
|
336
|
+
period: Trendbar period (M1, H1, D1, etc.).
|
|
337
|
+
from_timestamp: Start of time range (inclusive).
|
|
338
|
+
to_timestamp: End of time range (inclusive).
|
|
339
|
+
timeout: Request timeout (uses default if None).
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
List of Trendbar objects, ordered by timestamp ascending.
|
|
343
|
+
|
|
344
|
+
Note:
|
|
345
|
+
The server may limit the number of bars returned per request.
|
|
346
|
+
For large ranges, consider paginating with smaller time windows.
|
|
347
|
+
|
|
348
|
+
Raises:
|
|
349
|
+
APIError: If request fails.
|
|
350
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
351
|
+
"""
|
|
352
|
+
request = ProtoOAGetTrendbarsReq(
|
|
353
|
+
ctid_trader_account_id=account_id,
|
|
354
|
+
symbol_id=symbol_id,
|
|
355
|
+
period=ProtoOATrendbarPeriod(_PERIOD_TO_PROTO[period]),
|
|
356
|
+
from_timestamp=int(from_timestamp.timestamp() * 1000),
|
|
357
|
+
to_timestamp=int(to_timestamp.timestamp() * 1000),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
response = await self._protocol.send_request(
|
|
361
|
+
request,
|
|
362
|
+
timeout=timeout or self._default_timeout,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if not isinstance(response, ProtoOAGetTrendbarsRes):
|
|
366
|
+
raise APIError(
|
|
367
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
368
|
+
description=f"Expected ProtoOAGetTrendbarsRes, got {type(response).__name__}",
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return [Trendbar.from_proto(t) for t in response.trendbar]
|
|
372
|
+
|
|
373
|
+
async def get_tick_data(
|
|
374
|
+
self,
|
|
375
|
+
account_id: int,
|
|
376
|
+
symbol_id: int,
|
|
377
|
+
from_timestamp: datetime,
|
|
378
|
+
to_timestamp: datetime,
|
|
379
|
+
quote_type: str = "BID",
|
|
380
|
+
timeout: float | None = None,
|
|
381
|
+
) -> list[TickData]:
|
|
382
|
+
"""Get historical tick data.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
account_id: The cTID trader account ID.
|
|
386
|
+
symbol_id: Symbol to get data for.
|
|
387
|
+
from_timestamp: Start of time range (inclusive).
|
|
388
|
+
to_timestamp: End of time range (inclusive).
|
|
389
|
+
quote_type: "BID" or "ASK".
|
|
390
|
+
timeout: Request timeout (uses default if None).
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
List of TickData objects, ordered by timestamp ascending.
|
|
394
|
+
|
|
395
|
+
Note:
|
|
396
|
+
Tick data can be voluminous. Use small time windows to avoid
|
|
397
|
+
timeout issues and excessive memory usage.
|
|
398
|
+
|
|
399
|
+
Raises:
|
|
400
|
+
APIError: If request fails.
|
|
401
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
402
|
+
"""
|
|
403
|
+
qt = ProtoOAQuoteType.BID if quote_type.upper() == "BID" else ProtoOAQuoteType.ASK
|
|
404
|
+
|
|
405
|
+
request = ProtoOAGetTickDataReq(
|
|
406
|
+
ctid_trader_account_id=account_id,
|
|
407
|
+
symbol_id=symbol_id,
|
|
408
|
+
type=qt,
|
|
409
|
+
from_timestamp=int(from_timestamp.timestamp() * 1000),
|
|
410
|
+
to_timestamp=int(to_timestamp.timestamp() * 1000),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
response = await self._protocol.send_request(
|
|
414
|
+
request,
|
|
415
|
+
timeout=timeout or self._default_timeout,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if not isinstance(response, ProtoOAGetTickDataRes):
|
|
419
|
+
raise APIError(
|
|
420
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
421
|
+
description=f"Expected ProtoOAGetTickDataRes, got {type(response).__name__}",
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return [TickData.from_proto(t) for t in response.tick_data]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .._internal.proto import (
|
|
6
|
+
ProtoOASymbolByIdReq,
|
|
7
|
+
ProtoOASymbolByIdRes,
|
|
8
|
+
ProtoOASymbolsListReq,
|
|
9
|
+
ProtoOASymbolsListRes,
|
|
10
|
+
)
|
|
11
|
+
from ..exceptions import APIError
|
|
12
|
+
from ..models import Symbol, SymbolInfo
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..connection import Protocol
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SymbolsAPI:
|
|
20
|
+
"""Symbol information and search operations.
|
|
21
|
+
|
|
22
|
+
Provides methods to list, retrieve, and search trading symbols.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
```python
|
|
26
|
+
# List all available symbols
|
|
27
|
+
symbols = await client.symbols.list_all(account_id)
|
|
28
|
+
|
|
29
|
+
# Search for EUR pairs
|
|
30
|
+
eur_pairs = await client.symbols.search(account_id, "EUR")
|
|
31
|
+
|
|
32
|
+
# Get full details for specific symbols
|
|
33
|
+
eurusd = await client.symbols.get_by_id(account_id, 270)
|
|
34
|
+
```
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, protocol: Protocol, default_timeout: float = 30.0) -> None:
|
|
38
|
+
"""Initialize the symbols API.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
protocol: The protocol instance for sending requests.
|
|
42
|
+
default_timeout: Default request timeout in seconds.
|
|
43
|
+
"""
|
|
44
|
+
self._protocol = protocol
|
|
45
|
+
self._default_timeout = default_timeout
|
|
46
|
+
|
|
47
|
+
async def list_all(
|
|
48
|
+
self,
|
|
49
|
+
account_id: int,
|
|
50
|
+
timeout: float | None = None,
|
|
51
|
+
) -> list[SymbolInfo]:
|
|
52
|
+
"""List all available symbols (lightweight info).
|
|
53
|
+
|
|
54
|
+
Returns basic symbol information without full trading parameters.
|
|
55
|
+
Use `get_by_ids()` for complete symbol details.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
account_id: The cTID trader account ID.
|
|
59
|
+
timeout: Request timeout (uses default if None).
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
List of SymbolInfo objects with basic symbol data.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
APIError: If request fails.
|
|
66
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
67
|
+
"""
|
|
68
|
+
request = ProtoOASymbolsListReq(ctid_trader_account_id=account_id)
|
|
69
|
+
|
|
70
|
+
response = await self._protocol.send_request(
|
|
71
|
+
request,
|
|
72
|
+
timeout=timeout or self._default_timeout,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if not isinstance(response, ProtoOASymbolsListRes):
|
|
76
|
+
raise APIError(
|
|
77
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
78
|
+
description=f"Expected ProtoOASymbolsListRes, got {type(response).__name__}",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return [SymbolInfo.from_proto(s) for s in response.symbol]
|
|
82
|
+
|
|
83
|
+
async def get_by_ids(
|
|
84
|
+
self,
|
|
85
|
+
account_id: int,
|
|
86
|
+
symbol_ids: list[int],
|
|
87
|
+
timeout: float | None = None,
|
|
88
|
+
) -> list[Symbol]:
|
|
89
|
+
"""Get full symbol details by IDs.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
account_id: The cTID trader account ID.
|
|
93
|
+
symbol_ids: List of symbol IDs to retrieve.
|
|
94
|
+
timeout: Request timeout (uses default if None).
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of Symbol objects with full trading parameters.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
APIError: If request fails.
|
|
101
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
102
|
+
"""
|
|
103
|
+
request = ProtoOASymbolByIdReq(
|
|
104
|
+
ctid_trader_account_id=account_id,
|
|
105
|
+
symbol_id=symbol_ids,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
response = await self._protocol.send_request(
|
|
109
|
+
request,
|
|
110
|
+
timeout=timeout or self._default_timeout,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if not isinstance(response, ProtoOASymbolByIdRes):
|
|
114
|
+
raise APIError(
|
|
115
|
+
error_code="UNEXPECTED_RESPONSE",
|
|
116
|
+
description=f"Expected ProtoOASymbolByIdRes, got {type(response).__name__}",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return [Symbol.from_proto(s) for s in response.symbol]
|
|
120
|
+
|
|
121
|
+
async def get_by_id(
|
|
122
|
+
self,
|
|
123
|
+
account_id: int,
|
|
124
|
+
symbol_id: int,
|
|
125
|
+
timeout: float | None = None,
|
|
126
|
+
) -> Symbol:
|
|
127
|
+
"""Get a single symbol by ID.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
account_id: The cTID trader account ID.
|
|
131
|
+
symbol_id: The symbol ID.
|
|
132
|
+
timeout: Request timeout (uses default if None).
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Symbol with full trading parameters.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If symbol not found.
|
|
139
|
+
APIError: If request fails.
|
|
140
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
141
|
+
"""
|
|
142
|
+
symbols = await self.get_by_ids(account_id, [symbol_id], timeout)
|
|
143
|
+
if not symbols:
|
|
144
|
+
raise ValueError(f"Symbol {symbol_id} not found")
|
|
145
|
+
return symbols[0]
|
|
146
|
+
|
|
147
|
+
async def search(
|
|
148
|
+
self,
|
|
149
|
+
account_id: int,
|
|
150
|
+
query: str,
|
|
151
|
+
timeout: float | None = None,
|
|
152
|
+
) -> list[SymbolInfo]:
|
|
153
|
+
"""Search symbols by name.
|
|
154
|
+
|
|
155
|
+
Performs case-insensitive substring matching on symbol names.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
account_id: The cTID trader account ID.
|
|
159
|
+
query: Search string (e.g., "EUR", "BTCUSD").
|
|
160
|
+
timeout: Request timeout (uses default if None).
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of matching SymbolInfo objects.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
APIError: If request fails.
|
|
167
|
+
CTraderConnectionTimeoutError: If request times out.
|
|
168
|
+
"""
|
|
169
|
+
all_symbols = await self.list_all(account_id, timeout)
|
|
170
|
+
query_upper = query.upper()
|
|
171
|
+
return [s for s in all_symbols if query_upper in s.name.upper()]
|