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.
Files changed (43) hide show
  1. ctrader_api_client/__init__.py +64 -0
  2. ctrader_api_client/_internal/__init__.py +26 -0
  3. ctrader_api_client/_internal/messages.py +348 -0
  4. ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +42 -0
  5. ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +30 -0
  6. ctrader_api_client/_internal/proto/OpenApiMessages.py +1112 -0
  7. ctrader_api_client/_internal/proto/OpenApiModelMessages.py +802 -0
  8. ctrader_api_client/_internal/proto/__init__.py +320 -0
  9. ctrader_api_client/_internal/serialization.py +84 -0
  10. ctrader_api_client/api/__init__.py +21 -0
  11. ctrader_api_client/api/accounts.py +71 -0
  12. ctrader_api_client/api/market_data.py +424 -0
  13. ctrader_api_client/api/symbols.py +171 -0
  14. ctrader_api_client/api/trading.py +506 -0
  15. ctrader_api_client/auth/__init__.py +14 -0
  16. ctrader_api_client/auth/credentials.py +72 -0
  17. ctrader_api_client/auth/manager.py +511 -0
  18. ctrader_api_client/client.py +475 -0
  19. ctrader_api_client/config.py +56 -0
  20. ctrader_api_client/connection/__init__.py +16 -0
  21. ctrader_api_client/connection/heartbeat.py +120 -0
  22. ctrader_api_client/connection/protocol.py +366 -0
  23. ctrader_api_client/connection/transport.py +123 -0
  24. ctrader_api_client/enums.py +138 -0
  25. ctrader_api_client/events/__init__.py +65 -0
  26. ctrader_api_client/events/emitter.py +254 -0
  27. ctrader_api_client/events/router.py +400 -0
  28. ctrader_api_client/events/types.py +340 -0
  29. ctrader_api_client/exceptions.py +231 -0
  30. ctrader_api_client/models/__init__.py +50 -0
  31. ctrader_api_client/models/_base.py +19 -0
  32. ctrader_api_client/models/account.py +177 -0
  33. ctrader_api_client/models/deal.py +242 -0
  34. ctrader_api_client/models/market_data.py +192 -0
  35. ctrader_api_client/models/order.py +262 -0
  36. ctrader_api_client/models/position.py +209 -0
  37. ctrader_api_client/models/requests.py +299 -0
  38. ctrader_api_client/models/symbol.py +194 -0
  39. ctrader_api_client/py.typed +0 -0
  40. ctrader_api_client-0.1.0.dist-info/METADATA +252 -0
  41. ctrader_api_client-0.1.0.dist-info/RECORD +43 -0
  42. ctrader_api_client-0.1.0.dist-info/WHEEL +4 -0
  43. 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()]