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,262 @@
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 ProtoOAOrderStatus, ProtoOAOrderTriggerMethod, ProtoOAOrderType, ProtoOATimeInForce
8
+ from ..enums import OrderSide, OrderStatus, OrderType, StopTriggerMethod, TimeInForce
9
+ from ._base import FrozenModel
10
+
11
+
12
+ if TYPE_CHECKING:
13
+ from .._internal.proto import ProtoOAOrder
14
+ from .symbol import Symbol
15
+
16
+
17
+ def _timestamp_to_datetime(timestamp_ms: int) -> datetime:
18
+ """Convert millisecond timestamp to datetime."""
19
+ return datetime.fromtimestamp(timestamp_ms / 1000, tz=UTC)
20
+
21
+
22
+ _ORDER_TYPE_MAP: dict[int, OrderType] = {
23
+ ProtoOAOrderType.MARKET: OrderType.MARKET,
24
+ ProtoOAOrderType.LIMIT: OrderType.LIMIT,
25
+ ProtoOAOrderType.STOP: OrderType.STOP,
26
+ ProtoOAOrderType.STOP_LOSS_TAKE_PROFIT: OrderType.STOP_LOSS_TAKE_PROFIT,
27
+ ProtoOAOrderType.MARKET_RANGE: OrderType.MARKET_RANGE,
28
+ ProtoOAOrderType.STOP_LIMIT: OrderType.STOP_LIMIT,
29
+ }
30
+
31
+ _ORDER_STATUS_MAP: dict[int, OrderStatus] = {
32
+ ProtoOAOrderStatus.ORDER_STATUS_ACCEPTED: OrderStatus.ACCEPTED,
33
+ ProtoOAOrderStatus.ORDER_STATUS_FILLED: OrderStatus.FILLED,
34
+ ProtoOAOrderStatus.ORDER_STATUS_REJECTED: OrderStatus.REJECTED,
35
+ ProtoOAOrderStatus.ORDER_STATUS_EXPIRED: OrderStatus.EXPIRED,
36
+ ProtoOAOrderStatus.ORDER_STATUS_CANCELLED: OrderStatus.CANCELLED,
37
+ }
38
+
39
+ _TIME_IN_FORCE_MAP: dict[int, TimeInForce] = {
40
+ ProtoOATimeInForce.GOOD_TILL_DATE: TimeInForce.GOOD_TILL_DATE,
41
+ ProtoOATimeInForce.GOOD_TILL_CANCEL: TimeInForce.GOOD_TILL_CANCEL,
42
+ ProtoOATimeInForce.IMMEDIATE_OR_CANCEL: TimeInForce.IMMEDIATE_OR_CANCEL,
43
+ ProtoOATimeInForce.FILL_OR_KILL: TimeInForce.FILL_OR_KILL,
44
+ ProtoOATimeInForce.MARKET_ON_OPEN: TimeInForce.MARKET_ON_OPEN,
45
+ }
46
+
47
+ _TRIGGER_METHOD_MAP: dict[int, StopTriggerMethod] = {
48
+ ProtoOAOrderTriggerMethod.TRADE: StopTriggerMethod.TRADE,
49
+ ProtoOAOrderTriggerMethod.OPPOSITE: StopTriggerMethod.OPPOSITE,
50
+ ProtoOAOrderTriggerMethod.DOUBLE_TRADE: StopTriggerMethod.DOUBLE_TRADE,
51
+ ProtoOAOrderTriggerMethod.DOUBLE_OPPOSITE: StopTriggerMethod.DOUBLE_OPPOSITE,
52
+ }
53
+
54
+
55
+ class Order(FrozenModel):
56
+ """A trading order (pending or historical).
57
+
58
+ Represents an order with all details including type, prices, volume,
59
+ and execution information.
60
+
61
+ Attributes:
62
+ order_id: Unique order identifier.
63
+ symbol_id: The symbol being traded.
64
+ side: Order direction (BUY/SELL).
65
+ order_type: Type of order (MARKET, LIMIT, STOP, etc.).
66
+ status: Current order status.
67
+ volume: Order volume in cents.
68
+ time_in_force: Order duration type.
69
+ open_timestamp: When the order was created.
70
+ limit_price: Limit price as float, or None.
71
+ stop_price: Stop trigger price as float, or None.
72
+ stop_loss: Stop loss price as float, or None.
73
+ take_profit: Take profit price as float, or None.
74
+ execution_price: Average fill price as float, or None.
75
+ executed_volume: Volume that has been filled, in cents.
76
+ expiration_timestamp: When the order expires, or None.
77
+ position_id: Associated position ID, or None.
78
+ base_slippage_price: Base price for slippage calculation.
79
+ slippage_in_points: Max allowed slippage in points.
80
+ relative_stop_loss: Stop loss distance in points.
81
+ relative_take_profit: Take profit distance in points.
82
+ is_closing_order: Whether this order closes a position.
83
+ is_stop_out: Whether this order was triggered by stop-out.
84
+ trailing_stop_loss: Whether trailing stop is enabled.
85
+ guaranteed_stop_loss: Whether guaranteed stop loss is enabled.
86
+ stop_trigger_method: Method for triggering stop orders.
87
+ client_order_id: User-defined order ID.
88
+ label: User-defined label.
89
+ comment: User-defined comment.
90
+ last_update_timestamp: When the order was last modified.
91
+ """
92
+
93
+ order_id: int
94
+ symbol_id: int
95
+ side: OrderSide
96
+ order_type: OrderType
97
+ status: OrderStatus
98
+ volume: int
99
+ time_in_force: TimeInForce
100
+ open_timestamp: datetime
101
+
102
+ # Prices (as float from API)
103
+ limit_price: float | None = None
104
+ stop_price: float | None = None
105
+ stop_loss: float | None = None
106
+ take_profit: float | None = None
107
+ execution_price: float | None = None
108
+
109
+ # Execution
110
+ executed_volume: int = 0
111
+ expiration_timestamp: datetime | None = None
112
+ position_id: int | None = None
113
+
114
+ # Slippage
115
+ base_slippage_price: float | None = None
116
+ slippage_in_points: int | None = None
117
+
118
+ # Relative SL/TP (in points)
119
+ relative_stop_loss: int | None = None
120
+ relative_take_profit: int | None = None
121
+
122
+ # Flags
123
+ is_closing_order: bool = False
124
+ is_stop_out: bool = False
125
+ trailing_stop_loss: bool = False
126
+ guaranteed_stop_loss: bool = False
127
+ stop_trigger_method: StopTriggerMethod = StopTriggerMethod.TRADE
128
+
129
+ # Metadata
130
+ client_order_id: str = ""
131
+ label: str = ""
132
+ comment: str = ""
133
+ last_update_timestamp: datetime | None = None
134
+
135
+ def get_limit_price(self, symbol: Symbol) -> Decimal | None:
136
+ """Get limit price as Decimal.
137
+
138
+ Args:
139
+ symbol: Symbol for price precision.
140
+
141
+ Returns:
142
+ Limit price, or None if not set.
143
+ """
144
+ if self.limit_price is None:
145
+ return None
146
+ return Decimal(str(self.limit_price)).quantize(Decimal(10) ** -symbol.digits)
147
+
148
+ def get_stop_price(self, symbol: Symbol) -> Decimal | None:
149
+ """Get stop trigger price as Decimal.
150
+
151
+ Args:
152
+ symbol: Symbol for price precision.
153
+
154
+ Returns:
155
+ Stop price, or None if not set.
156
+ """
157
+ if self.stop_price is None:
158
+ return None
159
+ return Decimal(str(self.stop_price)).quantize(Decimal(10) ** -symbol.digits)
160
+
161
+ def get_execution_price(self, symbol: Symbol) -> Decimal | None:
162
+ """Get average execution price as Decimal.
163
+
164
+ Args:
165
+ symbol: Symbol for price precision.
166
+
167
+ Returns:
168
+ Execution price, or None if not filled.
169
+ """
170
+ if self.execution_price is None:
171
+ return None
172
+ return Decimal(str(self.execution_price)).quantize(Decimal(10) ** -symbol.digits)
173
+
174
+ def get_volume_in_lots(self, symbol: Symbol) -> Decimal:
175
+ """Get order volume in lots.
176
+
177
+ Args:
178
+ symbol: Symbol for lot conversion.
179
+
180
+ Returns:
181
+ Volume in lots.
182
+ """
183
+ return symbol.volume_to_lots(self.volume)
184
+
185
+ def get_executed_volume_in_lots(self, symbol: Symbol) -> Decimal:
186
+ """Get executed volume in lots.
187
+
188
+ Args:
189
+ symbol: Symbol for lot conversion.
190
+
191
+ Returns:
192
+ Executed volume in lots.
193
+ """
194
+ return symbol.volume_to_lots(self.executed_volume)
195
+
196
+ @property
197
+ def is_pending(self) -> bool:
198
+ """Whether this is a pending order."""
199
+ return self.status == OrderStatus.ACCEPTED
200
+
201
+ @property
202
+ def is_filled(self) -> bool:
203
+ """Whether this order has been fully filled."""
204
+ return self.status == OrderStatus.FILLED
205
+
206
+ @classmethod
207
+ def from_proto(cls, proto: ProtoOAOrder) -> Order:
208
+ """Create Order from proto message.
209
+
210
+ Args:
211
+ proto: The proto message to convert.
212
+
213
+ Returns:
214
+ A new Order instance.
215
+ """
216
+ trade_data = proto.trade_data
217
+
218
+ # Determine side from trade_data
219
+ side = OrderSide.BUY
220
+ if trade_data and trade_data.trade_side == 2:
221
+ side = OrderSide.SELL
222
+
223
+ # Get open timestamp
224
+ open_ts = datetime.now(UTC)
225
+ if trade_data and trade_data.open_timestamp:
226
+ open_ts = _timestamp_to_datetime(trade_data.open_timestamp)
227
+
228
+ return cls(
229
+ order_id=proto.order_id,
230
+ symbol_id=trade_data.symbol_id if trade_data else 0,
231
+ side=side,
232
+ order_type=_ORDER_TYPE_MAP.get(proto.order_type, OrderType.MARKET),
233
+ status=_ORDER_STATUS_MAP.get(proto.order_status, OrderStatus.ACCEPTED),
234
+ volume=trade_data.volume if trade_data else 0,
235
+ time_in_force=_TIME_IN_FORCE_MAP.get(proto.time_in_force, TimeInForce.GOOD_TILL_CANCEL),
236
+ open_timestamp=open_ts,
237
+ limit_price=proto.limit_price if proto.limit_price else None,
238
+ stop_price=proto.stop_price if proto.stop_price else None,
239
+ stop_loss=proto.stop_loss if proto.stop_loss else None,
240
+ take_profit=proto.take_profit if proto.take_profit else None,
241
+ execution_price=proto.execution_price if proto.execution_price else None,
242
+ executed_volume=proto.executed_volume if proto.executed_volume else 0,
243
+ expiration_timestamp=(
244
+ _timestamp_to_datetime(proto.expiration_timestamp) if proto.expiration_timestamp else None
245
+ ),
246
+ position_id=proto.position_id if proto.position_id else None,
247
+ base_slippage_price=proto.base_slippage_price if proto.base_slippage_price else None,
248
+ slippage_in_points=proto.slippage_in_points if proto.slippage_in_points else None,
249
+ relative_stop_loss=proto.relative_stop_loss if proto.relative_stop_loss else None,
250
+ relative_take_profit=proto.relative_take_profit if proto.relative_take_profit else None,
251
+ is_closing_order=proto.closing_order,
252
+ is_stop_out=proto.is_stop_out,
253
+ trailing_stop_loss=proto.trailing_stop_loss,
254
+ guaranteed_stop_loss=trade_data.guaranteed_stop_loss if trade_data else False,
255
+ stop_trigger_method=_TRIGGER_METHOD_MAP.get(proto.stop_trigger_method, StopTriggerMethod.TRADE),
256
+ client_order_id=proto.client_order_id or "",
257
+ label=trade_data.label if trade_data else "",
258
+ comment=trade_data.comment if trade_data else "",
259
+ last_update_timestamp=(
260
+ _timestamp_to_datetime(proto.utc_last_update_timestamp) if proto.utc_last_update_timestamp else None
261
+ ),
262
+ )
@@ -0,0 +1,209 @@
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 ProtoOAOrderTriggerMethod, ProtoOAPositionStatus
8
+ from ..enums import OrderSide, PositionStatus, StopTriggerMethod
9
+ from ._base import FrozenModel
10
+
11
+
12
+ if TYPE_CHECKING:
13
+ from .._internal.proto import ProtoOAPosition
14
+ from .symbol import Symbol
15
+
16
+
17
+ def _timestamp_to_datetime(timestamp_ms: int) -> datetime:
18
+ """Convert millisecond timestamp to datetime."""
19
+ return datetime.fromtimestamp(timestamp_ms / 1000, tz=UTC)
20
+
21
+
22
+ _POSITION_STATUS_MAP: dict[int, PositionStatus] = {
23
+ ProtoOAPositionStatus.POSITION_STATUS_OPEN: PositionStatus.OPEN,
24
+ ProtoOAPositionStatus.POSITION_STATUS_CLOSED: PositionStatus.CLOSED,
25
+ ProtoOAPositionStatus.POSITION_STATUS_CREATED: PositionStatus.CREATED,
26
+ ProtoOAPositionStatus.POSITION_STATUS_ERROR: PositionStatus.ERROR,
27
+ }
28
+
29
+ _TRIGGER_METHOD_MAP: dict[int, StopTriggerMethod] = {
30
+ ProtoOAOrderTriggerMethod.TRADE: StopTriggerMethod.TRADE,
31
+ ProtoOAOrderTriggerMethod.OPPOSITE: StopTriggerMethod.OPPOSITE,
32
+ ProtoOAOrderTriggerMethod.DOUBLE_TRADE: StopTriggerMethod.DOUBLE_TRADE,
33
+ ProtoOAOrderTriggerMethod.DOUBLE_OPPOSITE: StopTriggerMethod.DOUBLE_OPPOSITE,
34
+ }
35
+
36
+
37
+ class Position(FrozenModel):
38
+ """An open or closed trading position.
39
+
40
+ Represents a position with all trading details including entry price,
41
+ stop loss, take profit, and P&L information.
42
+
43
+ Attributes:
44
+ position_id: Unique position identifier.
45
+ symbol_id: The symbol being traded.
46
+ side: Position direction (BUY/SELL).
47
+ volume: Position volume in cents.
48
+ entry_price: Entry price as float from API.
49
+ status: Current position status.
50
+ open_timestamp: When the position was opened.
51
+ money_digits: Decimal places for monetary values.
52
+ stop_loss: Stop loss price as float, or None if not set.
53
+ take_profit: Take profit price as float, or None if not set.
54
+ trailing_stop_loss: Whether trailing stop is enabled.
55
+ guaranteed_stop_loss: Whether guaranteed stop loss is enabled.
56
+ stop_loss_trigger_method: Method for triggering stop loss.
57
+ swap: Accumulated swap (raw integer).
58
+ commission: Total commission paid (raw integer).
59
+ used_margin: Margin allocated to position (raw integer).
60
+ margin_rate: Margin rate for the position.
61
+ label: User-defined label.
62
+ comment: User-defined comment.
63
+ last_update_timestamp: When the position was last updated.
64
+ close_timestamp: When the position was closed, if applicable.
65
+ """
66
+
67
+ position_id: int
68
+ symbol_id: int
69
+ side: OrderSide
70
+ volume: int
71
+ entry_price: float
72
+ status: PositionStatus
73
+ open_timestamp: datetime
74
+ money_digits: int
75
+
76
+ # Protection orders
77
+ stop_loss: float | None = None
78
+ take_profit: float | None = None
79
+ trailing_stop_loss: bool = False
80
+ guaranteed_stop_loss: bool = False
81
+ stop_loss_trigger_method: StopTriggerMethod = StopTriggerMethod.TRADE
82
+
83
+ # Financial
84
+ swap: int = 0
85
+ commission: int = 0
86
+ used_margin: int = 0
87
+ margin_rate: float | None = None
88
+
89
+ # Metadata
90
+ label: str = ""
91
+ comment: str = ""
92
+ last_update_timestamp: datetime | None = None
93
+ close_timestamp: datetime | None = None
94
+
95
+ def get_entry_price(self, symbol: Symbol) -> Decimal:
96
+ """Get entry price as Decimal.
97
+
98
+ Args:
99
+ symbol: Symbol for price precision.
100
+
101
+ Returns:
102
+ Entry price with correct decimal places.
103
+ """
104
+ return Decimal(str(self.entry_price)).quantize(Decimal(10) ** -symbol.digits)
105
+
106
+ def get_stop_loss(self, symbol: Symbol) -> Decimal | None:
107
+ """Get stop loss as Decimal.
108
+
109
+ Args:
110
+ symbol: Symbol for price precision.
111
+
112
+ Returns:
113
+ Stop loss price, or None if not set.
114
+ """
115
+ if self.stop_loss is None:
116
+ return None
117
+ return Decimal(str(self.stop_loss)).quantize(Decimal(10) ** -symbol.digits)
118
+
119
+ def get_take_profit(self, symbol: Symbol) -> Decimal | None:
120
+ """Get take profit as Decimal.
121
+
122
+ Args:
123
+ symbol: Symbol for price precision.
124
+
125
+ Returns:
126
+ Take profit price, or None if not set.
127
+ """
128
+ if self.take_profit is None:
129
+ return None
130
+ return Decimal(str(self.take_profit)).quantize(Decimal(10) ** -symbol.digits)
131
+
132
+ def get_swap(self) -> Decimal:
133
+ """Get accumulated swap as Decimal.
134
+
135
+ Returns:
136
+ Swap divided by 10^money_digits.
137
+ """
138
+ return Decimal(self.swap) / Decimal(10**self.money_digits)
139
+
140
+ def get_commission(self) -> Decimal:
141
+ """Get commission as Decimal.
142
+
143
+ Returns:
144
+ Commission divided by 10^money_digits.
145
+ """
146
+ return Decimal(self.commission) / Decimal(10**self.money_digits)
147
+
148
+ def get_volume_in_lots(self, symbol: Symbol) -> Decimal:
149
+ """Get volume in lots.
150
+
151
+ Args:
152
+ symbol: Symbol for lot conversion.
153
+
154
+ Returns:
155
+ Volume in lots.
156
+ """
157
+ return symbol.volume_to_lots(self.volume)
158
+
159
+ @classmethod
160
+ def from_proto(cls, proto: ProtoOAPosition) -> Position:
161
+ """Create Position from proto message.
162
+
163
+ Args:
164
+ proto: The proto message to convert.
165
+
166
+ Returns:
167
+ A new Position instance.
168
+ """
169
+ trade_data = proto.trade_data
170
+
171
+ # Determine side from trade_data
172
+ side = OrderSide.BUY
173
+ if trade_data and trade_data.trade_side == 2:
174
+ side = OrderSide.SELL
175
+
176
+ # Get open timestamp
177
+ open_ts = datetime.now(UTC)
178
+ if trade_data and trade_data.open_timestamp:
179
+ open_ts = _timestamp_to_datetime(trade_data.open_timestamp)
180
+
181
+ return cls(
182
+ position_id=proto.position_id,
183
+ symbol_id=trade_data.symbol_id if trade_data else 0,
184
+ side=side,
185
+ volume=trade_data.volume if trade_data else 0,
186
+ entry_price=proto.price if proto.price else 0.0,
187
+ status=_POSITION_STATUS_MAP.get(proto.position_status, PositionStatus.OPEN),
188
+ open_timestamp=open_ts,
189
+ money_digits=proto.money_digits if proto.money_digits else 2,
190
+ stop_loss=proto.stop_loss if proto.stop_loss else None,
191
+ take_profit=proto.take_profit if proto.take_profit else None,
192
+ trailing_stop_loss=proto.trailing_stop_loss,
193
+ guaranteed_stop_loss=proto.guaranteed_stop_loss,
194
+ stop_loss_trigger_method=_TRIGGER_METHOD_MAP.get(proto.stop_loss_trigger_method, StopTriggerMethod.TRADE),
195
+ swap=proto.swap if proto.swap else 0,
196
+ commission=proto.commission if proto.commission else 0,
197
+ used_margin=proto.used_margin if proto.used_margin else 0,
198
+ margin_rate=proto.margin_rate if proto.margin_rate else None,
199
+ label=trade_data.label if trade_data else "",
200
+ comment=trade_data.comment if trade_data else "",
201
+ last_update_timestamp=(
202
+ _timestamp_to_datetime(proto.utc_last_update_timestamp) if proto.utc_last_update_timestamp else None
203
+ ),
204
+ close_timestamp=(
205
+ _timestamp_to_datetime(trade_data.close_timestamp)
206
+ if trade_data and trade_data.close_timestamp
207
+ else None
208
+ ),
209
+ )