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,299 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+
5
+ from ctrader_api_client.enums import OrderSide, OrderType, StopTriggerMethod, TimeInForce
6
+
7
+ from .._internal.proto import (
8
+ ProtoOAAmendOrderReq,
9
+ ProtoOAAmendPositionSLTPReq,
10
+ ProtoOAClosePositionReq,
11
+ ProtoOANewOrderReq,
12
+ ProtoOAOrderTriggerMethod,
13
+ ProtoOAOrderType,
14
+ ProtoOAPayloadType,
15
+ ProtoOATimeInForce,
16
+ ProtoOATradeSide,
17
+ )
18
+ from ._base import FrozenModel
19
+
20
+
21
+ # Reverse mappings for to_proto conversions
22
+
23
+ _ORDER_TYPE_TO_PROTO: dict[OrderType, int] = {
24
+ OrderType.MARKET: ProtoOAOrderType.MARKET,
25
+ OrderType.LIMIT: ProtoOAOrderType.LIMIT,
26
+ OrderType.STOP: ProtoOAOrderType.STOP,
27
+ OrderType.STOP_LOSS_TAKE_PROFIT: ProtoOAOrderType.STOP_LOSS_TAKE_PROFIT,
28
+ OrderType.MARKET_RANGE: ProtoOAOrderType.MARKET_RANGE,
29
+ OrderType.STOP_LIMIT: ProtoOAOrderType.STOP_LIMIT,
30
+ }
31
+
32
+ _TIME_IN_FORCE_TO_PROTO: dict[TimeInForce, int] = {
33
+ TimeInForce.GOOD_TILL_DATE: ProtoOATimeInForce.GOOD_TILL_DATE,
34
+ TimeInForce.GOOD_TILL_CANCEL: ProtoOATimeInForce.GOOD_TILL_CANCEL,
35
+ TimeInForce.IMMEDIATE_OR_CANCEL: ProtoOATimeInForce.IMMEDIATE_OR_CANCEL,
36
+ TimeInForce.FILL_OR_KILL: ProtoOATimeInForce.FILL_OR_KILL,
37
+ TimeInForce.MARKET_ON_OPEN: ProtoOATimeInForce.MARKET_ON_OPEN,
38
+ }
39
+
40
+ _TRIGGER_TO_PROTO: dict[StopTriggerMethod, int] = {
41
+ StopTriggerMethod.TRADE: ProtoOAOrderTriggerMethod.TRADE,
42
+ StopTriggerMethod.OPPOSITE: ProtoOAOrderTriggerMethod.OPPOSITE,
43
+ StopTriggerMethod.DOUBLE_TRADE: ProtoOAOrderTriggerMethod.DOUBLE_TRADE,
44
+ StopTriggerMethod.DOUBLE_OPPOSITE: ProtoOAOrderTriggerMethod.DOUBLE_OPPOSITE,
45
+ }
46
+
47
+
48
+ class NewOrderRequest(FrozenModel):
49
+ """Request to place a new order.
50
+
51
+ Attributes:
52
+ symbol_id: Symbol to trade.
53
+ side: Order direction (BUY/SELL).
54
+ volume: Volume in cents (100 = 0.01 lots).
55
+ order_type: Type of order.
56
+ limit_price: Limit price for LIMIT/STOP_LIMIT orders.
57
+ stop_price: Stop trigger price for STOP/STOP_LIMIT orders.
58
+ stop_loss: Stop loss price.
59
+ take_profit: Take profit price.
60
+ time_in_force: Order duration type.
61
+ expiration_timestamp: Expiration for GTD orders.
62
+ position_id: Position ID to close (for closing orders).
63
+ client_order_id: User-defined order ID.
64
+ label: User-defined label (max 100 chars).
65
+ comment: User-defined comment (max 256 chars).
66
+ base_slippage_price: Base price for slippage calculation.
67
+ slippage_in_points: Max allowed slippage.
68
+ trailing_stop_loss: Enable trailing stop loss.
69
+ guaranteed_stop_loss: Enable guaranteed stop loss.
70
+ relative_stop_loss: Stop loss distance in points.
71
+ relative_take_profit: Take profit distance in points.
72
+ stop_trigger_method: How to trigger stop orders.
73
+ """
74
+
75
+ symbol_id: int
76
+ side: OrderSide
77
+ volume: int
78
+ order_type: OrderType = OrderType.MARKET
79
+
80
+ # Prices (as float for convenience)
81
+ limit_price: float | None = None
82
+ stop_price: float | None = None
83
+ stop_loss: float | None = None
84
+ take_profit: float | None = None
85
+
86
+ # Duration
87
+ time_in_force: TimeInForce = TimeInForce.GOOD_TILL_CANCEL
88
+ expiration_timestamp: datetime | None = None
89
+
90
+ # Position reference
91
+ position_id: int | None = None
92
+
93
+ # Metadata
94
+ client_order_id: str = ""
95
+ label: str = ""
96
+ comment: str = ""
97
+
98
+ # Slippage
99
+ base_slippage_price: float | None = None
100
+ slippage_in_points: int | None = None
101
+
102
+ # Relative SL/TP
103
+ relative_stop_loss: int | None = None
104
+ relative_take_profit: int | None = None
105
+
106
+ # Flags
107
+ trailing_stop_loss: bool = False
108
+ guaranteed_stop_loss: bool = False
109
+ stop_trigger_method: StopTriggerMethod | None = None
110
+
111
+ def to_proto(self, account_id: int) -> ProtoOANewOrderReq:
112
+ """Convert to proto message.
113
+
114
+ Args:
115
+ account_id: The trading account ID.
116
+
117
+ Returns:
118
+ Proto request message.
119
+ """
120
+
121
+ # Map enums to proto values
122
+ order_type_value = ProtoOAOrderType(_ORDER_TYPE_TO_PROTO[self.order_type])
123
+ trade_side_value = ProtoOATradeSide.BUY if self.side == OrderSide.BUY else ProtoOATradeSide.SELL
124
+ time_in_force_value = ProtoOATimeInForce(_TIME_IN_FORCE_TO_PROTO[self.time_in_force])
125
+
126
+ trigger_method = None
127
+ if self.stop_trigger_method:
128
+ trigger_method = ProtoOAOrderTriggerMethod(_TRIGGER_TO_PROTO[self.stop_trigger_method])
129
+
130
+ return ProtoOANewOrderReq(
131
+ payload_type=ProtoOAPayloadType.PROTO_OA_NEW_ORDER_REQ,
132
+ ctid_trader_account_id=account_id,
133
+ symbol_id=self.symbol_id,
134
+ order_type=order_type_value,
135
+ trade_side=trade_side_value,
136
+ volume=self.volume,
137
+ limit_price=self.limit_price or 0.0,
138
+ stop_price=self.stop_price or 0.0,
139
+ time_in_force=time_in_force_value,
140
+ expiration_timestamp=int(self.expiration_timestamp.timestamp() * 1000) if self.expiration_timestamp else 0,
141
+ stop_loss=self.stop_loss or 0.0,
142
+ take_profit=self.take_profit or 0.0,
143
+ comment=self.comment,
144
+ base_slippage_price=self.base_slippage_price or 0.0,
145
+ slippage_in_points=self.slippage_in_points or 0,
146
+ label=self.label,
147
+ position_id=self.position_id or 0,
148
+ client_order_id=self.client_order_id,
149
+ relative_stop_loss=self.relative_stop_loss or 0,
150
+ relative_take_profit=self.relative_take_profit or 0,
151
+ guaranteed_stop_loss=self.guaranteed_stop_loss,
152
+ trailing_stop_loss=self.trailing_stop_loss,
153
+ stop_trigger_method=trigger_method, # ty: ignore[invalid-argument-type]
154
+ )
155
+
156
+
157
+ class AmendOrderRequest(FrozenModel):
158
+ """Request to modify a pending order.
159
+
160
+ Only include fields you want to change.
161
+
162
+ Attributes:
163
+ order_id: The order to modify.
164
+ volume: New volume in cents.
165
+ limit_price: New limit price.
166
+ stop_price: New stop trigger price.
167
+ stop_loss: New stop loss price.
168
+ take_profit: New take profit price.
169
+ expiration_timestamp: New expiration time.
170
+ slippage_in_points: New max slippage.
171
+ trailing_stop_loss: Enable/disable trailing stop.
172
+ guaranteed_stop_loss: Enable/disable guaranteed stop.
173
+ relative_stop_loss: New relative stop loss in points.
174
+ relative_take_profit: New relative take profit in points.
175
+ stop_trigger_method: New trigger method.
176
+ """
177
+
178
+ order_id: int
179
+
180
+ # Optional updates
181
+ volume: int | None = None
182
+ limit_price: float | None = None
183
+ stop_price: float | None = None
184
+ stop_loss: float | None = None
185
+ take_profit: float | None = None
186
+ expiration_timestamp: datetime | None = None
187
+ slippage_in_points: int | None = None
188
+ trailing_stop_loss: bool | None = None
189
+ guaranteed_stop_loss: bool | None = None
190
+ relative_stop_loss: int | None = None
191
+ relative_take_profit: int | None = None
192
+ stop_trigger_method: StopTriggerMethod | None = None
193
+
194
+ def to_proto(self, account_id: int) -> ProtoOAAmendOrderReq:
195
+ """Convert to proto message.
196
+
197
+ Args:
198
+ account_id: The trading account ID.
199
+
200
+ Returns:
201
+ Proto request message.
202
+ """
203
+
204
+ trigger_method = ProtoOAOrderTriggerMethod.TRADE
205
+ if self.stop_trigger_method:
206
+ trigger_method = ProtoOAOrderTriggerMethod(_TRIGGER_TO_PROTO[self.stop_trigger_method])
207
+
208
+ return ProtoOAAmendOrderReq(
209
+ payload_type=ProtoOAPayloadType.PROTO_OA_AMEND_ORDER_REQ,
210
+ ctid_trader_account_id=account_id,
211
+ order_id=self.order_id,
212
+ volume=self.volume or 0,
213
+ limit_price=self.limit_price or 0.0,
214
+ stop_price=self.stop_price or 0.0,
215
+ expiration_timestamp=int(self.expiration_timestamp.timestamp() * 1000) if self.expiration_timestamp else 0,
216
+ stop_loss=self.stop_loss or 0.0,
217
+ take_profit=self.take_profit or 0.0,
218
+ slippage_in_points=self.slippage_in_points or 0,
219
+ relative_stop_loss=self.relative_stop_loss or 0,
220
+ relative_take_profit=self.relative_take_profit or 0,
221
+ guaranteed_stop_loss=self.guaranteed_stop_loss if self.guaranteed_stop_loss is not None else False,
222
+ trailing_stop_loss=self.trailing_stop_loss if self.trailing_stop_loss is not None else False,
223
+ stop_trigger_method=trigger_method,
224
+ )
225
+
226
+
227
+ class AmendPositionRequest(FrozenModel):
228
+ """Request to modify a position's stop loss and take profit.
229
+
230
+ Attributes:
231
+ position_id: The position to modify.
232
+ stop_loss: New stop loss price, or None to remove.
233
+ take_profit: New take profit price, or None to remove.
234
+ trailing_stop_loss: Enable/disable trailing stop.
235
+ guaranteed_stop_loss: Enable/disable guaranteed stop.
236
+ stop_loss_trigger_method: Trigger method for stop loss.
237
+ """
238
+
239
+ position_id: int
240
+ stop_loss: float | None = None
241
+ take_profit: float | None = None
242
+ trailing_stop_loss: bool = False
243
+ guaranteed_stop_loss: bool = False
244
+ stop_loss_trigger_method: StopTriggerMethod | None = None
245
+
246
+ def to_proto(self, account_id: int) -> ProtoOAAmendPositionSLTPReq:
247
+ """Convert to proto message.
248
+
249
+ Args:
250
+ account_id: The trading account ID.
251
+
252
+ Returns:
253
+ Proto request message.
254
+ """
255
+
256
+ trigger_method = ProtoOAOrderTriggerMethod.TRADE
257
+ if self.stop_loss_trigger_method:
258
+ trigger_method = ProtoOAOrderTriggerMethod(_TRIGGER_TO_PROTO[self.stop_loss_trigger_method])
259
+
260
+ return ProtoOAAmendPositionSLTPReq(
261
+ payload_type=ProtoOAPayloadType.PROTO_OA_AMEND_POSITION_SLTP_REQ,
262
+ ctid_trader_account_id=account_id,
263
+ position_id=self.position_id,
264
+ stop_loss=self.stop_loss or 0.0,
265
+ take_profit=self.take_profit or 0.0,
266
+ guaranteed_stop_loss=self.guaranteed_stop_loss,
267
+ trailing_stop_loss=self.trailing_stop_loss,
268
+ stop_loss_trigger_method=trigger_method,
269
+ )
270
+
271
+
272
+ class ClosePositionRequest(FrozenModel):
273
+ """Request to close a position.
274
+
275
+ Attributes:
276
+ position_id: The position to close.
277
+ volume: Volume to close in cents. Use position's full volume
278
+ for complete close, or partial volume for partial close.
279
+ """
280
+
281
+ position_id: int
282
+ volume: int
283
+
284
+ def to_proto(self, account_id: int) -> ProtoOAClosePositionReq:
285
+ """Convert to proto message.
286
+
287
+ Args:
288
+ account_id: The trading account ID.
289
+
290
+ Returns:
291
+ Proto request message.
292
+ """
293
+
294
+ return ProtoOAClosePositionReq(
295
+ payload_type=ProtoOAPayloadType.PROTO_OA_CLOSE_POSITION_REQ,
296
+ ctid_trader_account_id=account_id,
297
+ position_id=self.position_id,
298
+ volume=self.volume,
299
+ )
@@ -0,0 +1,194 @@
1
+ from __future__ import annotations
2
+
3
+ from decimal import Decimal
4
+ from typing import TYPE_CHECKING
5
+
6
+ from .._internal.proto import ProtoOATradingMode
7
+ from ..enums import TradingMode
8
+ from ._base import FrozenModel
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from .._internal.proto import ProtoOALightSymbol, ProtoOASymbol
13
+
14
+
15
+ _TRADING_MODE_MAP: dict[int, TradingMode] = {
16
+ ProtoOATradingMode.ENABLED: TradingMode.ENABLED,
17
+ ProtoOATradingMode.DISABLED_WITHOUT_PENDINGS_EXECUTION: TradingMode.DISABLED_WITHOUT_PENDINGS_EXECUTION,
18
+ ProtoOATradingMode.DISABLED_WITH_PENDINGS_EXECUTION: TradingMode.DISABLED_WITH_PENDINGS_EXECUTION,
19
+ ProtoOATradingMode.CLOSE_ONLY_MODE: TradingMode.CLOSE_ONLY,
20
+ }
21
+
22
+
23
+ class SymbolInfo(FrozenModel):
24
+ """Basic symbol information from symbol list.
25
+
26
+ This is the lightweight representation returned when listing symbols.
27
+ Use Symbol for full trading parameters.
28
+
29
+ Attributes:
30
+ symbol_id: The unique symbol identifier.
31
+ name: Symbol name (e.g., "EURUSD").
32
+ enabled: Whether the symbol is enabled for trading.
33
+ base_asset_id: Asset ID of the base currency.
34
+ quote_asset_id: Asset ID of the quote currency.
35
+ category_id: Symbol category ID.
36
+ description: Human-readable description.
37
+ """
38
+
39
+ symbol_id: int
40
+ name: str
41
+ enabled: bool
42
+ base_asset_id: int
43
+ quote_asset_id: int
44
+ category_id: int | None = None
45
+ description: str = ""
46
+
47
+ @classmethod
48
+ def from_proto(cls, proto: ProtoOALightSymbol) -> SymbolInfo:
49
+ """Create SymbolInfo from proto message.
50
+
51
+ Args:
52
+ proto: The proto message to convert.
53
+
54
+ Returns:
55
+ A new SymbolInfo instance.
56
+ """
57
+ return cls(
58
+ symbol_id=proto.symbol_id,
59
+ name=proto.symbol_name,
60
+ enabled=proto.enabled,
61
+ base_asset_id=proto.base_asset_id,
62
+ quote_asset_id=proto.quote_asset_id,
63
+ category_id=proto.symbol_category_id if proto.symbol_category_id else None,
64
+ description=proto.description or "",
65
+ )
66
+
67
+
68
+ class Symbol(FrozenModel):
69
+ """Full symbol trading parameters.
70
+
71
+ Contains all information needed for trading calculations including
72
+ pip position, lot size, volume limits, and swap rates.
73
+
74
+ Attributes:
75
+ symbol_id: The unique symbol identifier.
76
+ digits: Price decimal places.
77
+ pip_position: Position of pip in price (e.g., 4 means 0.0001).
78
+ lot_size: Contract size in base units (e.g., 100000 for forex).
79
+ min_volume: Minimum order volume in cents.
80
+ max_volume: Maximum order volume in cents.
81
+ step_volume: Volume step in cents.
82
+ trading_mode: Current trading mode.
83
+ swap_long: Swap rate for long positions.
84
+ swap_short: Swap rate for short positions.
85
+ commission: Commission rate.
86
+ max_exposure: Maximum allowed exposure.
87
+ leverage_id: ID of dynamic leverage profile, if any.
88
+ enable_short_selling: Whether short selling is allowed.
89
+ guaranteed_stop_loss: Whether guaranteed stop loss is available.
90
+ sl_distance: Minimum stop loss distance.
91
+ tp_distance: Minimum take profit distance.
92
+ schedule_timezone: Timezone for trading schedule.
93
+ measurement_units: Measurement units (e.g., "oz" for gold).
94
+ """
95
+
96
+ symbol_id: int
97
+ digits: int
98
+ pip_position: int
99
+ lot_size: int
100
+ min_volume: int
101
+ max_volume: int
102
+ step_volume: int
103
+ trading_mode: TradingMode
104
+ swap_long: float
105
+ swap_short: float
106
+
107
+ # Optional fields
108
+ commission: int = 0
109
+ max_exposure: int | None = None
110
+ leverage_id: int | None = None
111
+ enable_short_selling: bool = True
112
+ guaranteed_stop_loss: bool = False
113
+ sl_distance: int = 0
114
+ tp_distance: int = 0
115
+ schedule_timezone: str = ""
116
+ measurement_units: str = ""
117
+
118
+ def price_to_decimal(self, raw_price: int) -> Decimal:
119
+ """Convert raw price integer to Decimal.
120
+
121
+ Args:
122
+ raw_price: Raw price from API.
123
+
124
+ Returns:
125
+ Price as Decimal with correct precision.
126
+ """
127
+ return Decimal(raw_price) / Decimal(10**self.digits)
128
+
129
+ def decimal_to_price(self, price: Decimal) -> int:
130
+ """Convert Decimal price to raw integer.
131
+
132
+ Args:
133
+ price: Price as Decimal.
134
+
135
+ Returns:
136
+ Raw price integer for API.
137
+ """
138
+ return int(price * Decimal(10**self.digits))
139
+
140
+ @staticmethod
141
+ def volume_to_lots(volume_cents: int) -> Decimal:
142
+ """Convert volume in cents to lots.
143
+
144
+ Args:
145
+ volume_cents: Volume in cents (100 = 0.01 lots).
146
+
147
+ Returns:
148
+ Volume in lots.
149
+ """
150
+ return Decimal(volume_cents) / Decimal(100)
151
+
152
+ @staticmethod
153
+ def lots_to_volume(lots: Decimal) -> int:
154
+ """Convert lots to volume in cents.
155
+
156
+ Args:
157
+ lots: Volume in lots.
158
+
159
+ Returns:
160
+ Volume in cents for API.
161
+ """
162
+ return int(lots * 100)
163
+
164
+ @classmethod
165
+ def from_proto(cls, proto: ProtoOASymbol) -> Symbol:
166
+ """Create Symbol from proto message.
167
+
168
+ Args:
169
+ proto: The proto message to convert.
170
+
171
+ Returns:
172
+ A new Symbol instance.
173
+ """
174
+ return cls(
175
+ symbol_id=proto.symbol_id,
176
+ digits=proto.digits,
177
+ pip_position=proto.pip_position,
178
+ lot_size=proto.lot_size,
179
+ min_volume=proto.min_volume,
180
+ max_volume=proto.max_volume,
181
+ step_volume=proto.step_volume,
182
+ trading_mode=_TRADING_MODE_MAP.get(proto.trading_mode, TradingMode.ENABLED),
183
+ swap_long=proto.swap_long,
184
+ swap_short=proto.swap_short,
185
+ commission=proto.commission if proto.commission else 0,
186
+ max_exposure=proto.max_exposure if proto.max_exposure else None,
187
+ leverage_id=proto.leverage_id if proto.leverage_id else None,
188
+ enable_short_selling=proto.enable_short_selling,
189
+ guaranteed_stop_loss=proto.guaranteed_stop_loss,
190
+ sl_distance=proto.sl_distance if proto.sl_distance else 0,
191
+ tp_distance=proto.tp_distance if proto.tp_distance else 0,
192
+ schedule_timezone=proto.schedule_time_zone or "",
193
+ measurement_units=proto.measurement_units or "",
194
+ )
File without changes