ctrader-api-client 0.3.0__tar.gz → 0.4.0__tar.gz

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 (92) hide show
  1. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/PKG-INFO +1 -1
  2. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/docs/api/events.md +1 -1
  3. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/docs/api/market-data.md +3 -3
  4. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/docs/api/models.md +6 -6
  5. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/docs/index.md +1 -1
  6. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/pyproject.toml +3 -1
  7. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/api/trading.py +2 -2
  8. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/client.py +59 -0
  9. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/events/router.py +5 -3
  10. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/events/types.py +2 -2
  11. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/account.py +11 -8
  12. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/deal.py +25 -22
  13. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/market_data.py +16 -14
  14. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/order.py +19 -18
  15. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/position.py +20 -19
  16. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/requests.py +36 -34
  17. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/symbol.py +8 -7
  18. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/api/test_market_data_api.py +2 -1
  19. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/events/test_emitter.py +17 -16
  20. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/events/test_router.py +2 -2
  21. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/events/test_types.py +7 -7
  22. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/models/test_account.py +10 -9
  23. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/models/test_deal.py +23 -22
  24. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/models/test_market_data.py +21 -21
  25. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/models/test_order.py +5 -4
  26. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/models/test_position.py +8 -7
  27. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/models/test_requests.py +20 -19
  28. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/models/test_symbol.py +16 -15
  29. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/uv.lock +1 -1
  30. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/.claude/settings.local.json +0 -0
  31. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/.github/workflows/docs.yml +0 -0
  32. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/.gitignore +0 -0
  33. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/.pre-commit-config.yaml +0 -0
  34. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/.python-version +0 -0
  35. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/Justfile +0 -0
  36. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/LICENSE +0 -0
  37. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/README.md +0 -0
  38. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/docs/api/accounts.md +0 -0
  39. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/docs/api/client.md +0 -0
  40. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/docs/api/enums.md +0 -0
  41. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/docs/api/symbols.md +0 -0
  42. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/docs/api/trading.md +0 -0
  43. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/docs/getting-started.md +0 -0
  44. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/mkdocs.yml +0 -0
  45. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/protos/SOURCE +0 -0
  46. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/protos/VERSION +0 -0
  47. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/protos/update.sh +0 -0
  48. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
  49. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
  50. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/protos/vendor/OpenApiMessages.proto +0 -0
  51. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/protos/vendor/OpenApiModelMessages.proto +0 -0
  52. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/scripts/fix_proto_imports.py +0 -0
  53. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/__init__.py +0 -0
  54. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/__init__.py +0 -0
  55. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/messages.py +0 -0
  56. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
  57. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
  58. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
  59. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
  60. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
  61. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
  62. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/api/__init__.py +0 -0
  63. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/api/accounts.py +0 -0
  64. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/api/market_data.py +0 -0
  65. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/api/symbols.py +0 -0
  66. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/auth/__init__.py +0 -0
  67. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/auth/credentials.py +0 -0
  68. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/auth/manager.py +0 -0
  69. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/config.py +0 -0
  70. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/connection/__init__.py +0 -0
  71. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/connection/heartbeat.py +0 -0
  72. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/connection/protocol.py +0 -0
  73. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/connection/transport.py +0 -0
  74. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/enums.py +0 -0
  75. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/events/__init__.py +0 -0
  76. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/events/emitter.py +0 -0
  77. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/exceptions.py +0 -0
  78. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/__init__.py +0 -0
  79. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/_base.py +0 -0
  80. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/src/ctrader_api_client/py.typed +0 -0
  81. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/_internal/test_messages.py +0 -0
  82. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/_internal/test_serialization.py +0 -0
  83. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/api/conftest.py +0 -0
  84. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/api/test_accounts.py +0 -0
  85. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/api/test_symbols.py +0 -0
  86. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/api/test_trading.py +0 -0
  87. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/auth/test_credentials.py +0 -0
  88. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/auth/test_manager.py +0 -0
  89. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/connection/test_heartbeat.py +0 -0
  90. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/connection/test_protocol.py +0 -0
  91. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/connection/test_transport.py +0 -0
  92. {ctrader_api_client-0.3.0 → ctrader_api_client-0.4.0}/tests/unit/test_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctrader-api-client
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: API Client to interact with the cTrader Open API spec
5
5
  Author-email: Elio <elioachukri@pm.me>
6
6
  License-File: LICENSE
@@ -52,7 +52,7 @@ await client.market_data.subscribe_trendbars(account_id, 270, TrendbarPeriod.M1)
52
52
 
53
53
  @client.on(SpotEvent, symbol_id=270)
54
54
  async def on_spot(event: SpotEvent):
55
- # Prices are floats
55
+ # Prices are Decimals
56
56
  print(f"Bid: {event.bid}, Ask: {event.ask}")
57
57
 
58
58
  # Trendbar is included when subscribed
@@ -30,7 +30,7 @@ from ctrader_api_client.events import SpotEvent
30
30
  await client.market_data.subscribe_spots(account_id, [270, 271])
31
31
 
32
32
 
33
- # Handle price updates - bid/ask are floats
33
+ # Handle price updates - bid/ask are Decimals
34
34
  @client.on(SpotEvent, symbol_id=270)
35
35
  async def on_price(event: SpotEvent):
36
36
  print(f"Bid: {event.bid}, Ask: {event.ask}")
@@ -86,7 +86,7 @@ trendbars = await client.market_data.get_trendbars(
86
86
  to_timestamp=datetime.now(UTC),
87
87
  )
88
88
 
89
- # OHLC values are already floats (converted from raw integers)
89
+ # OHLC values are already Decimals (converted from raw integers)
90
90
  for bar in trendbars:
91
91
  print(f"{bar.timestamp}: O={bar.open} H={bar.high} L={bar.low} C={bar.close} V={bar.volume}")
92
92
  ```
@@ -104,7 +104,7 @@ ticks = await client.market_data.get_tick_data(
104
104
  quote_type="BID", # or "ASK"
105
105
  )
106
106
 
107
- # Price is already a float
107
+ # Price is already a Decimal
108
108
  for tick in ticks:
109
109
  print(f"{tick.timestamp}: {tick.price}")
110
110
  ```
@@ -166,30 +166,30 @@ symbol = await client.symbols.get_by_id(account_id, 270)
166
166
  volume = symbol.lots_to_volume(0.1) # Returns volume in cents
167
167
 
168
168
  # Convert volume to lots for display
169
- lots = symbol.volume_to_lots(position.volume) # Returns lots as float
169
+ lots = symbol.volume_to_lots(position.volume) # Returns lots as Decimal
170
170
  ```
171
171
 
172
172
  ## Price Values
173
173
 
174
- Prices in events and models (bid, ask, OHLC, execution prices, etc.) are returned as **floats** - no conversion needed:
174
+ Prices in events and models (bid, ask, OHLC, execution prices, etc.) are returned as **Decimals** - no conversion needed:
175
175
 
176
176
  ```python
177
177
  @client.on(SpotEvent)
178
178
  async def on_price(event: SpotEvent):
179
- # bid and ask are already floats
179
+ # bid and ask are already Decimals
180
180
  spread = event.ask - event.bid
181
181
  print(f"Spread: {spread}")
182
182
 
183
- # Trendbar OHLC are floats
183
+ # Trendbar OHLC are Decimals
184
184
  for bar in trendbars:
185
185
  range_size = bar.high - bar.low
186
186
  print(f"Range: {range_size}")
187
187
 
188
- # Account balance is a float
188
+ # Account balance is a Decimal
189
189
  account = await client.accounts.get_trader(account_id)
190
190
  print(f"Balance: {account.balance}")
191
191
 
192
- # Position values are floats
192
+ # Position values are Decimals
193
193
  for pos in positions:
194
194
  print(f"Swap: {pos.swap}, Commission: {pos.commission}")
195
195
  ```
@@ -41,7 +41,7 @@ client = CTraderClient(config)
41
41
 
42
42
  @client.on(SpotEvent, symbol_id=270) # Filter by symbol
43
43
  async def on_price(event: SpotEvent):
44
- # bid and ask are floats
44
+ # bid and ask are Decimals
45
45
  print(f"Price update: {event.bid}/{event.ask}")
46
46
 
47
47
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctrader-api-client"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "API Client to interact with the cTrader Open API spec"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -51,6 +51,8 @@ select = [
51
51
  "C4", # comprehensions
52
52
  "ARG", # Flags unused function arguments
53
53
  "RUF022", # Unsorted __all__ blocks
54
+ "FURB157", # Check unnecessary casts in Decimal constructors
55
+ "RUF032", # Check for Decimal calls passing a float literal
54
56
  ]
55
57
 
56
58
  [tool.ruff.lint.per-file-ignores]
@@ -194,8 +194,8 @@ class TradingAPI:
194
194
  return [
195
195
  PositionUnrealizedPnL(
196
196
  position_id=p.position_id,
197
- gross_unrealized_pnl=p.gross_unrealized_pn_l / divisor,
198
- net_unrealized_pnl=p.net_unrealized_pn_l / divisor,
197
+ gross_unrealized_pnl=Decimal(p.gross_unrealized_pn_l) / divisor,
198
+ net_unrealized_pnl=Decimal(p.net_unrealized_pn_l) / divisor,
199
199
  )
200
200
  for p in response.position_unrealized_pn_l
201
201
  ]
@@ -454,6 +454,65 @@ class CTraderClient:
454
454
 
455
455
  return decorator
456
456
 
457
+ @overload
458
+ def register_handler(
459
+ self,
460
+ event_type: type[T_BothFilters],
461
+ handler: EventHandler[T_BothFilters],
462
+ *,
463
+ account_id: int | None = ...,
464
+ symbol_id: int | None = ...,
465
+ ) -> None: ...
466
+
467
+ @overload
468
+ def register_handler(
469
+ self,
470
+ event_type: type[T_AccountIdOnly],
471
+ handler: EventHandler[T_AccountIdOnly],
472
+ *,
473
+ account_id: int | None = ...,
474
+ ) -> None: ...
475
+
476
+ @overload
477
+ def register_handler(
478
+ self,
479
+ event_type: type[T_NoFilters],
480
+ handler: EventHandler[T_NoFilters],
481
+ ) -> None: ...
482
+
483
+ def register_handler(
484
+ self,
485
+ event_type: type[T],
486
+ handler: EventHandler[T],
487
+ *,
488
+ account_id: int | None = None,
489
+ symbol_id: int | None = None,
490
+ ) -> None:
491
+ """Register an event handler.
492
+
493
+ Same as the on() decorator but as a regular method for dynamic registration.
494
+
495
+ Args:
496
+ event_type: The event class to listen for.
497
+ handler: The async function to call when the event arrives.
498
+ account_id: Only receive events for this account (optional).
499
+ symbol_id: Only receive events for this symbol (optional).
500
+ Example:
501
+ ```python
502
+ async def on_eurusd(event: SpotEvent) -> None:
503
+ print(f"EURUSD: {event.bid}/{event.ask}")
504
+
505
+
506
+ client.register_handler(SpotEvent, on_eurusd, symbol_id=270)
507
+ ```
508
+ """
509
+ self._emitter.subscribe(
510
+ event_type,
511
+ handler,
512
+ account_id=account_id,
513
+ symbol_id=symbol_id,
514
+ )
515
+
457
516
  def off(
458
517
  self,
459
518
  event_type: type[T],
@@ -183,10 +183,12 @@ class EventRouter:
183
183
  event = SpotEvent(
184
184
  account_id=proto.ctid_trader_account_id,
185
185
  symbol_id=proto.symbol_id,
186
- bid=proto.bid / 1e5 if proto.bid else None,
187
- ask=proto.ask / 1e5 if proto.ask else None,
186
+ bid=proto.bid / Decimal(100000) if proto.bid else None,
187
+ ask=proto.ask / Decimal(100000) if proto.ask else None,
188
188
  timestamp=self._timestamp_to_datetime(proto.timestamp) if proto.timestamp else datetime.now(UTC),
189
- trendbar=Trendbar.from_proto(proto.trendbar[0], bid_price=proto.bid) if proto.trendbar else None,
189
+ trendbar=(
190
+ Trendbar.from_proto(proto.trendbar[0], bid_price=Decimal(str(proto.bid))) if proto.trendbar else None
191
+ ),
190
192
  )
191
193
  await self._emitter.emit(event)
192
194
 
@@ -30,8 +30,8 @@ class SpotEvent:
30
30
 
31
31
  account_id: int
32
32
  symbol_id: int
33
- bid: float | None
34
- ask: float | None
33
+ bid: Decimal | None
34
+ ask: Decimal | None
35
35
  trendbar: Trendbar | None
36
36
  timestamp: datetime
37
37
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import UTC, datetime
4
+ from decimal import Decimal
4
5
  from typing import TYPE_CHECKING
5
6
 
6
7
  from .._internal.proto import ProtoOAAccessRights, ProtoOAAccountType
@@ -107,7 +108,7 @@ class Account(FrozenModel):
107
108
 
108
109
  account_id: int
109
110
  trader_login: int
110
- balance: float
111
+ balance: Decimal
111
112
  leverage_in_cents: int
112
113
  account_type: AccountType
113
114
  access_rights: AccessRights
@@ -120,9 +121,9 @@ class Account(FrozenModel):
120
121
  # Optional fields
121
122
  max_leverage: int | None = None
122
123
  balance_version: int | None = None
123
- manager_bonus: float | None = None
124
- ib_bonus: float | None = None
125
- non_withdrawable_bonus: float | None = None
124
+ manager_bonus: Decimal | None = None
125
+ ib_bonus: Decimal | None = None
126
+ non_withdrawable_bonus: Decimal | None = None
126
127
 
127
128
  def get_leverage(self) -> str:
128
129
  """Get leverage as human-readable string.
@@ -148,7 +149,7 @@ class Account(FrozenModel):
148
149
  return cls(
149
150
  account_id=proto.ctid_trader_account_id,
150
151
  trader_login=proto.trader_login,
151
- balance=proto.balance / divisor,
152
+ balance=Decimal(proto.balance) / divisor,
152
153
  leverage_in_cents=proto.leverage_in_cents,
153
154
  account_type=_ACCOUNT_TYPE_MAP.get(proto.account_type, AccountType.HEDGED),
154
155
  access_rights=_ACCESS_RIGHTS_MAP.get(proto.access_rights, AccessRights.FULL_ACCESS),
@@ -161,7 +162,9 @@ class Account(FrozenModel):
161
162
  ),
162
163
  max_leverage=proto.max_leverage if proto.max_leverage else None,
163
164
  balance_version=proto.balance_version if proto.balance_version else None,
164
- manager_bonus=proto.manager_bonus / divisor if proto.manager_bonus else None,
165
- ib_bonus=proto.ib_bonus / divisor if proto.ib_bonus else None,
166
- non_withdrawable_bonus=proto.non_withdrawable_bonus / divisor if proto.non_withdrawable_bonus else None,
165
+ manager_bonus=Decimal(proto.manager_bonus) / divisor if proto.manager_bonus else None,
166
+ ib_bonus=Decimal(proto.ib_bonus) / divisor if proto.ib_bonus else None,
167
+ non_withdrawable_bonus=Decimal(proto.non_withdrawable_bonus) / divisor
168
+ if proto.non_withdrawable_bonus
169
+ else None,
167
170
  )
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import UTC, datetime
4
+ from decimal import Decimal
4
5
  from typing import TYPE_CHECKING
5
6
 
6
7
  from .._internal.proto import ProtoOADealStatus
@@ -45,14 +46,14 @@ class CloseDetail(FrozenModel):
45
46
  balance_version: Version number for balance updates.
46
47
  """
47
48
 
48
- entry_price: float
49
+ entry_price: Decimal
49
50
  closed_volume: int
50
- gross_profit: float
51
- swap: float
52
- commission: float
53
- balance: float
54
- pnl_conversion_fee: float = 0
55
- quote_to_deposit_rate: float | None = None
51
+ gross_profit: Decimal
52
+ swap: Decimal
53
+ commission: Decimal
54
+ balance: Decimal
55
+ pnl_conversion_fee: Decimal = Decimal(0)
56
+ quote_to_deposit_rate: Decimal | None = None
56
57
  balance_version: int | None = None
57
58
 
58
59
  @classmethod
@@ -68,14 +69,14 @@ class CloseDetail(FrozenModel):
68
69
  money_digits = proto.money_digits if proto.money_digits else 2
69
70
  divisor = 10**money_digits
70
71
  return cls(
71
- entry_price=proto.entry_price,
72
+ entry_price=Decimal(str(proto.entry_price)),
72
73
  closed_volume=proto.closed_volume,
73
- gross_profit=proto.gross_profit / divisor,
74
- swap=proto.swap / divisor,
75
- commission=proto.commission / divisor,
76
- balance=proto.balance / divisor,
77
- pnl_conversion_fee=proto.pnl_conversion_fee / divisor if proto.pnl_conversion_fee else 0,
78
- quote_to_deposit_rate=proto.quote_to_deposit_conversion_rate
74
+ gross_profit=Decimal(proto.gross_profit) / divisor,
75
+ swap=Decimal(proto.swap) / divisor,
76
+ commission=Decimal(proto.commission) / divisor,
77
+ balance=Decimal(proto.balance) / divisor,
78
+ pnl_conversion_fee=Decimal(proto.pnl_conversion_fee) / divisor if proto.pnl_conversion_fee else Decimal(0),
79
+ quote_to_deposit_rate=Decimal(str(proto.quote_to_deposit_conversion_rate))
79
80
  if proto.quote_to_deposit_conversion_rate
80
81
  else None,
81
82
  balance_version=proto.balance_version if proto.balance_version else None,
@@ -114,16 +115,16 @@ class Deal(FrozenModel):
114
115
  side: OrderSide
115
116
  volume: int
116
117
  filled_volume: int
117
- execution_price: float
118
+ execution_price: Decimal
118
119
  execution_timestamp: datetime
119
120
  status: DealStatus
120
- commission: float
121
+ commission: Decimal
121
122
 
122
123
  # Optional
123
124
  create_timestamp: datetime | None = None
124
125
  last_update_timestamp: datetime | None = None
125
- margin_rate: float | None = None
126
- base_to_usd_rate: float | None = None
126
+ margin_rate: Decimal | None = None
127
+ base_to_usd_rate: Decimal | None = None
127
128
  close_detail: CloseDetail | None = None
128
129
 
129
130
  @property
@@ -163,15 +164,17 @@ class Deal(FrozenModel):
163
164
  side=side,
164
165
  volume=proto.volume,
165
166
  filled_volume=proto.filled_volume,
166
- execution_price=proto.execution_price,
167
+ execution_price=Decimal(str(proto.execution_price)),
167
168
  execution_timestamp=_timestamp_to_datetime(proto.execution_timestamp),
168
169
  status=_DEAL_STATUS_MAP.get(proto.deal_status, DealStatus.FILLED),
169
- commission=proto.commission / divisor if proto.commission else 0,
170
+ commission=Decimal(proto.commission) / divisor if proto.commission else Decimal(0),
170
171
  create_timestamp=_timestamp_to_datetime(proto.create_timestamp) if proto.create_timestamp else None,
171
172
  last_update_timestamp=(
172
173
  _timestamp_to_datetime(proto.utc_last_update_timestamp) if proto.utc_last_update_timestamp else None
173
174
  ),
174
- margin_rate=proto.margin_rate if proto.margin_rate else None,
175
- base_to_usd_rate=proto.base_to_usd_conversion_rate if proto.base_to_usd_conversion_rate else None,
175
+ margin_rate=Decimal(str(proto.margin_rate)) if proto.margin_rate else None,
176
+ base_to_usd_rate=(
177
+ Decimal(str(proto.base_to_usd_conversion_rate)) if proto.base_to_usd_conversion_rate else None
178
+ ),
176
179
  close_detail=close_detail,
177
180
  )
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Sequence
4
4
  from datetime import UTC, datetime
5
+ from decimal import Decimal
5
6
  from typing import TYPE_CHECKING
6
7
 
7
8
  from .._internal.proto import ProtoOATrendbarPeriod
@@ -49,14 +50,14 @@ class Trendbar(FrozenModel):
49
50
 
50
51
  timestamp: datetime
51
52
  period: TrendbarPeriod
52
- open: float
53
- high: float
54
- low: float
55
- close: float
53
+ open: Decimal
54
+ high: Decimal
55
+ low: Decimal
56
+ close: Decimal
56
57
  volume: int
57
58
 
58
59
  @classmethod
59
- def from_proto(cls, proto: ProtoOATrendbar, bid_price: float | None = None, historical: bool = False) -> Trendbar:
60
+ def from_proto(cls, proto: ProtoOATrendbar, bid_price: Decimal | None = None, historical: bool = False) -> Trendbar:
60
61
  """Create a Trendbar from a proto message.
61
62
 
62
63
  Args:
@@ -89,13 +90,14 @@ class Trendbar(FrozenModel):
89
90
  )
90
91
  close = low + proto.delta_close
91
92
 
93
+ divisor = Decimal(100000)
92
94
  return cls(
93
95
  timestamp=ts,
94
96
  period=_PERIOD_MAP.get(proto.period, TrendbarPeriod.M1),
95
- low=low / 1e5,
96
- open=open_price / 1e5,
97
- high=high / 1e5,
98
- close=close / 1e5,
97
+ low=Decimal(low) / divisor,
98
+ open=Decimal(open_price) / divisor,
99
+ high=Decimal(high) / divisor,
100
+ close=Decimal(close) / divisor,
99
101
  volume=proto.volume,
100
102
  )
101
103
 
@@ -111,13 +113,13 @@ class TickData(FrozenModel):
111
113
  """
112
114
 
113
115
  timestamp: datetime
114
- price: float
116
+ price: Decimal
115
117
 
116
118
  @classmethod
117
119
  def from_proto(cls, proto: ProtoOATickData) -> TickData:
118
120
  """Create TickData from proto message.
119
121
 
120
- Note that price needs to be converted from a raw integer to a float by dividing by 1e5.
122
+ Note that price needs to be converted from a raw integer to a Decimal by dividing by 100000.
121
123
 
122
124
  Args:
123
125
  proto: The proto message.
@@ -128,7 +130,7 @@ class TickData(FrozenModel):
128
130
  # Proto timestamp is delta from base in milliseconds
129
131
  return cls(
130
132
  timestamp=datetime.fromtimestamp(proto.timestamp / 1000, tz=UTC),
131
- price=proto.tick / 1e5,
133
+ price=Decimal(proto.tick) / Decimal(100000),
132
134
  )
133
135
 
134
136
  @classmethod
@@ -139,7 +141,7 @@ class TickData(FrozenModel):
139
141
  the first data point being an absolute value.
140
142
  This method converts them to absolute values.
141
143
 
142
- Additionally, price needs to be converted from a raw integer to a float by dividing by 1e5.
144
+ Additionally, price needs to be converted from a raw integer to a Decimal by dividing by 100000.
143
145
 
144
146
  Args:
145
147
  protos: List of proto tick data messages.
@@ -161,7 +163,7 @@ class TickData(FrozenModel):
161
163
  ticks.append(
162
164
  cls(
163
165
  timestamp=datetime.fromtimestamp(current_timestamp / 1000, tz=UTC),
164
- price=current_price / 1e5,
166
+ price=Decimal(current_price) / Decimal(100000),
165
167
  )
166
168
  )
167
169
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import UTC, datetime
4
+ from decimal import Decimal
4
5
  from typing import TYPE_CHECKING
5
6
 
6
7
  from .._internal.proto import ProtoOAOrderStatus, ProtoOAOrderTriggerMethod, ProtoOAOrderType, ProtoOATimeInForce
@@ -65,11 +66,11 @@ class Order(FrozenModel):
65
66
  volume: Order volume in cents.
66
67
  time_in_force: Order duration type.
67
68
  open_timestamp: When the order was created.
68
- limit_price: Limit price as float, or None.
69
- stop_price: Stop trigger price as float, or None.
70
- stop_loss: Stop loss price as float, or None.
71
- take_profit: Take profit price as float, or None.
72
- execution_price: Average fill price as float, or None.
69
+ limit_price: Limit price, or None.
70
+ stop_price: Stop trigger price, or None.
71
+ stop_loss: Stop loss price, or None.
72
+ take_profit: Take profit price, or None.
73
+ execution_price: Average fill price, or None.
73
74
  executed_volume: Volume that has been filled, in cents.
74
75
  expiration_timestamp: When the order expires, or None.
75
76
  position_id: Associated position ID, or None.
@@ -97,12 +98,12 @@ class Order(FrozenModel):
97
98
  time_in_force: TimeInForce
98
99
  open_timestamp: datetime
99
100
 
100
- # Prices (as float from API)
101
- limit_price: float | None = None
102
- stop_price: float | None = None
103
- stop_loss: float | None = None
104
- take_profit: float | None = None
105
- execution_price: float | None = None
101
+ # Prices
102
+ limit_price: Decimal | None = None
103
+ stop_price: Decimal | None = None
104
+ stop_loss: Decimal | None = None
105
+ take_profit: Decimal | None = None
106
+ execution_price: Decimal | None = None
106
107
 
107
108
  # Execution
108
109
  executed_volume: int = 0
@@ -110,7 +111,7 @@ class Order(FrozenModel):
110
111
  position_id: int | None = None
111
112
 
112
113
  # Slippage
113
- base_slippage_price: float | None = None
114
+ base_slippage_price: Decimal | None = None
114
115
  slippage_in_points: int | None = None
115
116
 
116
117
  # Relative SL/TP (in points)
@@ -171,17 +172,17 @@ class Order(FrozenModel):
171
172
  volume=trade_data.volume if trade_data else 0,
172
173
  time_in_force=_TIME_IN_FORCE_MAP.get(proto.time_in_force, TimeInForce.GOOD_TILL_CANCEL),
173
174
  open_timestamp=open_ts,
174
- limit_price=proto.limit_price if proto.limit_price else None,
175
- stop_price=proto.stop_price if proto.stop_price else None,
176
- stop_loss=proto.stop_loss if proto.stop_loss else None,
177
- take_profit=proto.take_profit if proto.take_profit else None,
178
- execution_price=proto.execution_price if proto.execution_price else None,
175
+ limit_price=Decimal(str(proto.limit_price)) if proto.limit_price else None,
176
+ stop_price=Decimal(str(proto.stop_price)) if proto.stop_price else None,
177
+ stop_loss=Decimal(str(proto.stop_loss)) if proto.stop_loss else None,
178
+ take_profit=Decimal(str(proto.take_profit)) if proto.take_profit else None,
179
+ execution_price=Decimal(str(proto.execution_price)) if proto.execution_price else None,
179
180
  executed_volume=proto.executed_volume if proto.executed_volume else 0,
180
181
  expiration_timestamp=(
181
182
  _timestamp_to_datetime(proto.expiration_timestamp) if proto.expiration_timestamp else None
182
183
  ),
183
184
  position_id=proto.position_id if proto.position_id else None,
184
- base_slippage_price=proto.base_slippage_price if proto.base_slippage_price else None,
185
+ base_slippage_price=Decimal(str(proto.base_slippage_price)) if proto.base_slippage_price else None,
185
186
  slippage_in_points=proto.slippage_in_points if proto.slippage_in_points else None,
186
187
  relative_stop_loss=proto.relative_stop_loss if proto.relative_stop_loss else None,
187
188
  relative_take_profit=proto.relative_take_profit if proto.relative_take_profit else None,
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import UTC, datetime
4
+ from decimal import Decimal
4
5
  from typing import TYPE_CHECKING
5
6
 
6
7
  from .._internal.proto import ProtoOAOrderTriggerMethod, ProtoOAPositionStatus
@@ -43,12 +44,12 @@ class Position(FrozenModel):
43
44
  symbol_id: The symbol being traded.
44
45
  side: Position direction (BUY/SELL).
45
46
  volume: Position volume in cents.
46
- entry_price: Entry price as float from API.
47
+ entry_price: Entry price as Decimal from API.
47
48
  status: Current position status.
48
49
  open_timestamp: When the position was opened.
49
50
  money_digits: Decimal places for monetary values.
50
- stop_loss: Stop loss price as float, or None if not set.
51
- take_profit: Take profit price as float, or None if not set.
51
+ stop_loss: Stop loss price as Decimal, or None if not set.
52
+ take_profit: Take profit price as Decimal, or None if not set.
52
53
  trailing_stop_loss: Whether trailing stop is enabled.
53
54
  guaranteed_stop_loss: Whether guaranteed stop loss is enabled.
54
55
  stop_loss_trigger_method: Method for triggering stop loss.
@@ -66,23 +67,23 @@ class Position(FrozenModel):
66
67
  symbol_id: int
67
68
  side: OrderSide
68
69
  volume: int
69
- entry_price: float
70
+ entry_price: Decimal
70
71
  status: PositionStatus
71
72
  open_timestamp: datetime
72
73
  money_digits: int
73
74
 
74
75
  # Protection orders
75
- stop_loss: float | None = None
76
- take_profit: float | None = None
76
+ stop_loss: Decimal | None = None
77
+ take_profit: Decimal | None = None
77
78
  trailing_stop_loss: bool = False
78
79
  guaranteed_stop_loss: bool = False
79
80
  stop_loss_trigger_method: StopTriggerMethod = StopTriggerMethod.TRADE
80
81
 
81
82
  # Financial
82
- swap: float = 0
83
- commission: float = 0
84
- used_margin: float = 0
85
- margin_rate: float | None = None
83
+ swap: Decimal = Decimal(0)
84
+ commission: Decimal = Decimal(0)
85
+ used_margin: Decimal = Decimal(0)
86
+ margin_rate: Decimal | None = None
86
87
 
87
88
  # Metadata
88
89
  label: str = ""
@@ -120,19 +121,19 @@ class Position(FrozenModel):
120
121
  symbol_id=trade_data.symbol_id if trade_data else 0,
121
122
  side=side,
122
123
  volume=trade_data.volume if trade_data else 0,
123
- entry_price=proto.price if proto.price else 0.0,
124
+ entry_price=Decimal(str(proto.price)) if proto.price else Decimal(0),
124
125
  status=_POSITION_STATUS_MAP.get(proto.position_status, PositionStatus.OPEN),
125
126
  open_timestamp=open_ts,
126
127
  money_digits=proto.money_digits if proto.money_digits else 2,
127
- stop_loss=proto.stop_loss if proto.stop_loss else None,
128
- take_profit=proto.take_profit if proto.take_profit else None,
128
+ stop_loss=Decimal(str(proto.stop_loss)) if proto.stop_loss else None,
129
+ take_profit=Decimal(str(proto.take_profit)) if proto.take_profit else None,
129
130
  trailing_stop_loss=proto.trailing_stop_loss,
130
131
  guaranteed_stop_loss=proto.guaranteed_stop_loss,
131
132
  stop_loss_trigger_method=_TRIGGER_METHOD_MAP.get(proto.stop_loss_trigger_method, StopTriggerMethod.TRADE),
132
- swap=proto.swap / divisor if proto.swap else 0,
133
- commission=proto.commission / divisor if proto.commission else 0,
134
- used_margin=proto.used_margin / divisor if proto.used_margin else 0,
135
- margin_rate=proto.margin_rate if proto.margin_rate else None,
133
+ swap=Decimal(proto.swap) / divisor if proto.swap else Decimal(0),
134
+ commission=Decimal(proto.commission) / divisor if proto.commission else Decimal(0),
135
+ used_margin=Decimal(proto.used_margin) / divisor if proto.used_margin else Decimal(0),
136
+ margin_rate=Decimal(str(proto.margin_rate)) if proto.margin_rate else None,
136
137
  label=trade_data.label if trade_data else "",
137
138
  comment=trade_data.comment if trade_data else "",
138
139
  last_update_timestamp=(
@@ -157,5 +158,5 @@ class PositionUnrealizedPnL(FrozenModel):
157
158
  """
158
159
 
159
160
  position_id: int
160
- gross_unrealized_pnl: float
161
- net_unrealized_pnl: float
161
+ gross_unrealized_pnl: Decimal
162
+ net_unrealized_pnl: Decimal