ctrader-api-client 0.2.3__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 (94) hide show
  1. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/Justfile +4 -0
  2. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/PKG-INFO +1 -1
  3. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/docs/api/accounts.md +0 -1
  4. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/docs/api/events.md +1 -5
  5. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/docs/api/market-data.md +3 -3
  6. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/docs/api/models.md +9 -6
  7. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/docs/api/trading.md +10 -0
  8. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/docs/index.md +1 -1
  9. ctrader_api_client-0.4.0/protos/VERSION +1 -0
  10. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/protos/vendor/OpenApiMessages.proto +5 -38
  11. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/protos/vendor/OpenApiModelMessages.proto +1 -7
  12. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/pyproject.toml +3 -1
  13. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/messages.py +0 -10
  14. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +1 -51
  15. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +1 -7
  16. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -10
  17. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/api/market_data.py +10 -0
  18. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/api/trading.py +36 -14
  19. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/client.py +59 -2
  20. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/connection/heartbeat.py +3 -0
  21. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/connection/protocol.py +8 -3
  22. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/connection/transport.py +16 -2
  23. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/events/__init__.py +0 -2
  24. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/events/router.py +22 -22
  25. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/events/types.py +2 -23
  26. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/__init__.py +2 -1
  27. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/account.py +11 -8
  28. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/deal.py +25 -22
  29. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/market_data.py +16 -14
  30. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/order.py +19 -18
  31. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/position.py +33 -17
  32. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/requests.py +36 -34
  33. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/symbol.py +8 -7
  34. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/api/test_market_data_api.py +2 -1
  35. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/api/test_trading.py +85 -1
  36. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/events/test_emitter.py +17 -16
  37. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/events/test_router.py +2 -33
  38. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/events/test_types.py +7 -26
  39. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/models/test_account.py +10 -9
  40. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/models/test_deal.py +23 -22
  41. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/models/test_market_data.py +21 -21
  42. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/models/test_order.py +5 -4
  43. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/models/test_position.py +8 -7
  44. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/models/test_requests.py +20 -19
  45. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/models/test_symbol.py +16 -15
  46. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/uv.lock +1 -1
  47. ctrader_api_client-0.2.3/protos/VERSION +0 -1
  48. ctrader_api_client-0.2.3/protos/vendor/.gitkeep +0 -0
  49. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/.claude/settings.local.json +0 -0
  50. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/.github/workflows/docs.yml +0 -0
  51. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/.gitignore +0 -0
  52. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/.pre-commit-config.yaml +0 -0
  53. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/.python-version +0 -0
  54. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/LICENSE +0 -0
  55. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/README.md +0 -0
  56. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/docs/api/client.md +0 -0
  57. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/docs/api/enums.md +0 -0
  58. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/docs/api/symbols.md +0 -0
  59. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/docs/getting-started.md +0 -0
  60. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/mkdocs.yml +0 -0
  61. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/protos/SOURCE +0 -0
  62. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/protos/update.sh +0 -0
  63. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
  64. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
  65. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/scripts/fix_proto_imports.py +0 -0
  66. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/__init__.py +0 -0
  67. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/__init__.py +0 -0
  68. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
  69. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
  70. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
  71. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/api/__init__.py +0 -0
  72. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/api/accounts.py +0 -0
  73. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/api/symbols.py +0 -0
  74. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/auth/__init__.py +0 -0
  75. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/auth/credentials.py +0 -0
  76. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/auth/manager.py +0 -0
  77. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/config.py +0 -0
  78. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/connection/__init__.py +0 -0
  79. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/enums.py +0 -0
  80. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/events/emitter.py +0 -0
  81. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/exceptions.py +0 -0
  82. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/models/_base.py +0 -0
  83. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/src/ctrader_api_client/py.typed +0 -0
  84. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/_internal/test_messages.py +0 -0
  85. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/_internal/test_serialization.py +0 -0
  86. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/api/conftest.py +0 -0
  87. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/api/test_accounts.py +0 -0
  88. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/api/test_symbols.py +0 -0
  89. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/auth/test_credentials.py +0 -0
  90. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/auth/test_manager.py +0 -0
  91. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/connection/test_heartbeat.py +0 -0
  92. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/connection/test_protocol.py +0 -0
  93. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/connection/test_transport.py +0 -0
  94. {ctrader_api_client-0.2.3 → ctrader_api_client-0.4.0}/tests/unit/test_client.py +0 -0
@@ -10,6 +10,10 @@ default: help
10
10
  help:
11
11
  just --list
12
12
 
13
+ # Spin up documentation server using MkDocs
14
+ documentation:
15
+ uv run mkdocs serve
16
+
13
17
  # Run all CI steps: linting, formatting, type checking
14
18
  ci directory='':
15
19
  @just lint {{directory}}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctrader-api-client
3
- Version: 0.2.3
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
@@ -41,4 +41,3 @@ for acc in accounts:
41
41
  ## Related
42
42
 
43
43
  - [Authentication](client.md#authentication) - Authenticating accounts
44
- - [Models - Account](models.md#account) - Account model reference
@@ -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
@@ -108,10 +108,6 @@ async def on_ready(event: ReadyEvent):
108
108
  options:
109
109
  show_source: false
110
110
 
111
- ::: ctrader_api_client.events.PnLChangeEvent
112
- options:
113
- show_source: false
114
-
115
111
  ::: ctrader_api_client.events.TrailingStopChangedEvent
116
112
  options:
117
113
  show_source: false
@@ -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
  ```
@@ -102,6 +102,9 @@ partial_close = ClosePositionRequest(
102
102
  options:
103
103
  show_source: false
104
104
 
105
+ ::: ctrader_api_client.models.PositionUnrealizedPnL
106
+ options:
107
+ show_source: false
105
108
 
106
109
  ::: ctrader_api_client.models.Order
107
110
  options:
@@ -163,30 +166,30 @@ symbol = await client.symbols.get_by_id(account_id, 270)
163
166
  volume = symbol.lots_to_volume(0.1) # Returns volume in cents
164
167
 
165
168
  # Convert volume to lots for display
166
- lots = symbol.volume_to_lots(position.volume) # Returns lots as float
169
+ lots = symbol.volume_to_lots(position.volume) # Returns lots as Decimal
167
170
  ```
168
171
 
169
172
  ## Price Values
170
173
 
171
- 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:
172
175
 
173
176
  ```python
174
177
  @client.on(SpotEvent)
175
178
  async def on_price(event: SpotEvent):
176
- # bid and ask are already floats
179
+ # bid and ask are already Decimals
177
180
  spread = event.ask - event.bid
178
181
  print(f"Spread: {spread}")
179
182
 
180
- # Trendbar OHLC are floats
183
+ # Trendbar OHLC are Decimals
181
184
  for bar in trendbars:
182
185
  range_size = bar.high - bar.low
183
186
  print(f"Range: {range_size}")
184
187
 
185
- # Account balance is a float
188
+ # Account balance is a Decimal
186
189
  account = await client.accounts.get_trader(account_id)
187
190
  print(f"Balance: {account.balance}")
188
191
 
189
- # Position values are floats
192
+ # Position values are Decimals
190
193
  for pos in positions:
191
194
  print(f"Swap: {pos.swap}, Commission: {pos.commission}")
192
195
  ```
@@ -10,6 +10,7 @@ Access via `client.trading`.
10
10
  options:
11
11
  show_source: false
12
12
  members:
13
+ - get_unrealized_pnl_per_position
13
14
  - place_order
14
15
  - amend_order
15
16
  - cancel_order
@@ -175,3 +176,12 @@ for deal in deals:
175
176
  if deal.is_closing_deal and deal.close_detail:
176
177
  print(f" Gross P/L: {deal.close_detail.gross_profit}")
177
178
  ```
179
+
180
+ ### Get PnL for all open positions
181
+
182
+ ```python
183
+ unrealized_pnl = await client.trading.get_unrealized_pnl_per_position(account_id)
184
+
185
+ for p in unrealized_pnl:
186
+ print(f"Position {p.position_id}: Gross Unrealized PnL = {p.gross_unrealized_pnl}, Net Unrealized PnL = {p.net_unrealized_pnl}")
187
+ ```
@@ -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
 
@@ -0,0 +1 @@
1
+ main
@@ -91,8 +91,8 @@ message ProtoOANewOrderReq {
91
91
  optional double stopPrice = 8; // Stop Price, can be specified for the STOP and the STOP_LIMIT orders only.
92
92
  optional ProtoOATimeInForce timeInForce = 9 [default = GOOD_TILL_CANCEL]; // The specific order execution or expiration instruction - GOOD_TILL_DATE, GOOD_TILL_CANCEL, IMMEDIATE_OR_CANCEL, FILL_OR_KILL, MARKET_ON_OPEN.
93
93
  optional int64 expirationTimestamp = 10; // The Unix time in milliseconds of Order expiration. Should be set for the Good Till Date orders.
94
- optional double stopLoss = 11; // The absolute Stop Loss price (1.23456 for example). Not supported for the MARKER orders.
95
- optional double takeProfit = 12; // The absolute Take Profit price (1.23456 for example). Unsupported for the MARKER orders.
94
+ optional double stopLoss = 11; // The absolute Stop Loss price (1.23456 for example). Not supported for MARKET orders.
95
+ optional double takeProfit = 12; // The absolute Take Profit price (1.23456 for example). Unsupported for MARKET orders.
96
96
  optional string comment = 13; // User-specified comment. MaxLength = 512.
97
97
  optional double baseSlippagePrice = 14; // Base price to calculate relative slippage price for MARKET_RANGE order.
98
98
  optional int32 slippageInPoints = 15; // Slippage distance for MARKET_RANGE and STOP_LIMIT order.
@@ -139,8 +139,8 @@ message ProtoOAAmendOrderReq {
139
139
  optional double limitPrice = 5; // The Limit Price, can be specified for the LIMIT order only.
140
140
  optional double stopPrice = 6; // The Stop Price, can be specified for the STOP and the STOP_LIMIT orders.
141
141
  optional int64 expirationTimestamp = 7; // The Unix timestamp in milliseconds of Order expiration. Should be set for the Good Till Date orders.
142
- optional double stopLoss = 8; // The absolute Stop Loss price (e.g. 1.23456). Not supported for the MARKER orders.
143
- optional double takeProfit = 9; // The absolute Take Profit price (e.g. 1.23456). Not supported for the MARKER orders.
142
+ optional double stopLoss = 8; // The absolute Stop Loss price (e.g. 1.23456). Not supported for MARKET orders.
143
+ optional double takeProfit = 9; // The absolute Take Profit price (e.g. 1.23456). Not supported for MARKET orders.
144
144
  optional int32 slippageInPoints = 10; // Slippage distance for the MARKET_RANGE and the STOP_LIMIT orders.
145
145
  optional int64 relativeStopLoss = 11; // The relative Stop Loss can be specified instead of the absolute one. Specified in 1/100000 of a unit of price. (e.g. 123000 in protocol means 1.23, 53423782 means 534.23782) For BUY stopLoss = entryPrice - relativeStopLoss, for SELL stopLoss = entryPrice + relativeStopLoss.
146
146
  optional int64 relativeTakeProfit = 12; // The relative Take Profit can be specified instead of the absolute one. Specified in 1/100000 of a unit of price. (e.g. 123000 in protocol means 1.23, 53423782 means 534.23782) For BUY takeProfit = entryPrice + relativeTakeProfit, for SELL takeProfit = entryPrice - relativeTakeProfit.
@@ -637,7 +637,7 @@ message ProtoOAAccountLogoutReq {
637
637
  required int64 ctidTraderAccountId = 2; // The unique identifier of the trader's account in cTrader platform.
638
638
  }
639
639
 
640
- /** Response to the ProtoOATraderLogoutReq request. Actual logout of trading account will be completed on ProtoOAAccountDisconnectEvent. */
640
+ /** Response to the ProtoOAAccountLogoutReq request. Actual logout of trading account will be completed on ProtoOAAccountDisconnectEvent. */
641
641
  message ProtoOAAccountLogoutRes {
642
642
  optional ProtoOAPayloadType payloadType = 1 [default = PROTO_OA_ACCOUNT_LOGOUT_RES];
643
643
 
@@ -793,36 +793,3 @@ message ProtoOAGetPositionUnrealizedPnLRes {
793
793
  repeated ProtoOAPositionUnrealizedPnL positionUnrealizedPnL = 3; // Information about trader's positions' unrealized PnLs.
794
794
  required uint32 moneyDigits = 4; // Specifies the exponent of various monetary values. E.g., moneyDigits = 8 should be interpreted as the value multiplied by 10^8 with the 'real' value equal to 10053099944 / 10^8 = 100.53099944. Affects positionUnrealizedPnL.grossUnrealizedPnL, positionUnrealizedPnL.netUnrealizedPnL.
795
795
  }
796
-
797
- // The event that is sent when the unrealized PnL is changed due to market movement. Requires subscribing to PnL events, see ProtoOAv1PnLChangeSubscribeReq
798
- message ProtoOAv1PnLChangeEvent {
799
- optional ProtoOAPayloadType payloadType = 1 [default = PROTO_OA_V1_PNL_CHANGE_EVENT];
800
- required int64 ctidTraderAccountId = 2; //Unique identifier of the trader's account. Used to match responses to trader's accounts.
801
- required int64 grossUnrealizedPnL = 3; //The gross unrealized PnL denoted in the account deposit currency
802
- required int64 netUnrealizedPnL = 4; //The net unrealized PnL denoted in the account deposit currency
803
- required uint32 moneyDigits = 5; //Specifies the exponent of various monetary values. E.g., moneyDigits = 8 should be interpreted as the value multiplied by 10^8 with the 'real' value equal to 10053099944 / 10^8 = 100.53099944
804
- }
805
-
806
- //The request to subscribe to ProtoOAv1PnLChangeEvent
807
- message ProtoOAv1PnLChangeSubscribeReq {
808
- optional ProtoOAPayloadType payloadType = 1 [default = PROTO_OA_V1_PNL_CHANGE_SUBSCRIBE_REQ];
809
- required int64 ctidTraderAccountId = 2; //Unique identifier of the trader's account. Used to match responses to trader's accounts.
810
- }
811
-
812
- //The response for ProtoOAv1PnLChangeSubscribeReq
813
- message ProtoOAv1PnLChangeSubscribeRes {
814
- optional ProtoOAPayloadType payloadType = 1 [default = PROTO_OA_V1_PNL_CHANGE_SUBSCRIBE_RES];
815
- required int64 ctidTraderAccountId = 2; // The unique identifier of the trader's account in cTrader platform.
816
- }
817
-
818
- //The request to stop an existing subscription to PnL events. The subscriber who sends this request will stop receiving ProtoOAv1PnLChangeEvent
819
- message ProtoOAv1PnLChangeUnSubscribeReq {
820
- optional ProtoOAPayloadType payloadType = 1 [default = PROTO_OA_V1_PNL_CHANGE_UN_SUBSCRIBE_REQ];
821
- required int64 ctidTraderAccountId = 2; //Unique identifier of the trader's account. Used to match responses to trader's accounts.
822
- }
823
-
824
- //The response for ProtoOAv1PnLChangeUnSubscribeReq
825
- message ProtoOAv1PnLChangeUnSubscribeRes {
826
- optional ProtoOAPayloadType payloadType = 1 [default = PROTO_OA_V1_PNL_CHANGE_UN_SUBSCRIBE_RES];
827
- required int64 ctidTraderAccountId = 2; //Unique identifier of the trader's account. Used to match responses to trader's accounts.
828
- }
@@ -100,11 +100,6 @@ enum ProtoOAPayloadType {
100
100
  PROTO_OA_DEAL_OFFSET_LIST_RES = 2186;
101
101
  PROTO_OA_GET_POSITION_UNREALIZED_PNL_REQ = 2187;
102
102
  PROTO_OA_GET_POSITION_UNREALIZED_PNL_RES = 2188;
103
- PROTO_OA_V1_PNL_CHANGE_EVENT = 2189;
104
- PROTO_OA_V1_PNL_CHANGE_SUBSCRIBE_REQ = 2190;
105
- PROTO_OA_V1_PNL_CHANGE_SUBSCRIBE_RES = 2191;
106
- PROTO_OA_V1_PNL_CHANGE_UN_SUBSCRIBE_REQ = 2192;
107
- PROTO_OA_V1_PNL_CHANGE_UN_SUBSCRIBE_RES = 2193;
108
103
  }
109
104
 
110
105
  /** Asset entity. */
@@ -328,7 +323,7 @@ enum ProtoOAOrderStatus {
328
323
  ORDER_STATUS_CANCELLED = 5; // Order is cancelled. Might be valid for orders with partially filled volume that were cancelled by LP.
329
324
  }
330
325
 
331
- /** Stop Order and Stop Lost triggering method ENUM. */
326
+ /** Stop Order and Stop Loss triggering method ENUM. */
332
327
  enum ProtoOAOrderTriggerMethod {
333
328
  TRADE = 1; // Stop Order: buy is triggered by ask, sell by bid; Stop Loss Order: for buy position is triggered by bid and for sell position by ask.
334
329
  OPPOSITE = 2; // Stop Order: buy is triggered by bid, sell by ask; Stop Loss Order: for buy position is triggered by ask and for sell position by bid.
@@ -687,7 +682,6 @@ enum ProtoOAErrorCode {
687
682
  UNABLE_TO_CANCEL_ORDER = 134; // Unable to cancel order.
688
683
  UNABLE_TO_AMEND_ORDER = 135; // Unable to amend order.
689
684
  SHORT_SELLING_NOT_ALLOWED = 136; // Short selling is not allowed.
690
- NOT_SUBSCRIBED_TO_PNL = 137;//This session is not subscribed via ProtoOAv1PnLChangeSubscribeReq
691
685
  }
692
686
 
693
687
  enum ProtoOALimitedRiskMarginCalculationStrategy {
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctrader-api-client"
3
- version = "0.2.3"
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]
@@ -100,11 +100,6 @@ from .proto import (
100
100
  ProtoOAUnsubscribeLiveTrendbarRes,
101
101
  ProtoOAUnsubscribeSpotsReq,
102
102
  ProtoOAUnsubscribeSpotsRes,
103
- ProtoOAv1PnLChangeEvent,
104
- ProtoOAv1PnLChangeSubscribeReq,
105
- ProtoOAv1PnLChangeSubscribeRes,
106
- ProtoOAv1PnLChangeUnSubscribeReq,
107
- ProtoOAv1PnLChangeUnSubscribeRes,
108
103
  ProtoOAVersionReq,
109
104
  ProtoOAVersionRes,
110
105
  ProtoPayloadType,
@@ -207,11 +202,6 @@ _PAYLOAD_TYPE_TO_CLASS: dict[int, type[betterproto.Message]] = {
207
202
  ProtoOAPayloadType.PROTO_OA_DEAL_OFFSET_LIST_RES: ProtoOADealOffsetListRes,
208
203
  ProtoOAPayloadType.PROTO_OA_GET_POSITION_UNREALIZED_PNL_REQ: ProtoOAGetPositionUnrealizedPnLReq,
209
204
  ProtoOAPayloadType.PROTO_OA_GET_POSITION_UNREALIZED_PNL_RES: ProtoOAGetPositionUnrealizedPnLRes,
210
- ProtoOAPayloadType.PROTO_OA_V1_PNL_CHANGE_EVENT: ProtoOAv1PnLChangeEvent,
211
- ProtoOAPayloadType.PROTO_OA_V1_PNL_CHANGE_SUBSCRIBE_REQ: ProtoOAv1PnLChangeSubscribeReq,
212
- ProtoOAPayloadType.PROTO_OA_V1_PNL_CHANGE_SUBSCRIBE_RES: ProtoOAv1PnLChangeSubscribeRes,
213
- ProtoOAPayloadType.PROTO_OA_V1_PNL_CHANGE_UN_SUBSCRIBE_REQ: ProtoOAv1PnLChangeUnSubscribeReq,
214
- ProtoOAPayloadType.PROTO_OA_V1_PNL_CHANGE_UN_SUBSCRIBE_RES: ProtoOAv1PnLChangeUnSubscribeRes,
215
205
  }
216
206
 
217
207
 
@@ -850,7 +850,7 @@ class ProtoOAAccountLogoutReq(betterproto.Message):
850
850
  @dataclass
851
851
  class ProtoOAAccountLogoutRes(betterproto.Message):
852
852
  """
853
- * Response to the ProtoOATraderLogoutReq request. Actual logout of trading
853
+ * Response to the ProtoOAAccountLogoutReq request. Actual logout of trading
854
854
  account will be completed on ProtoOAAccountDisconnectEvent.
855
855
  """
856
856
 
@@ -1060,53 +1060,3 @@ class ProtoOAGetPositionUnrealizedPnLRes(betterproto.Message):
1060
1060
  ctid_trader_account_id: int = betterproto.int64_field(2)
1061
1061
  position_unrealized_pn_l: list["ProtoOAPositionUnrealizedPnL"] = betterproto.message_field(3)
1062
1062
  money_digits: int = betterproto.uint32_field(4)
1063
-
1064
-
1065
- @dataclass
1066
- class ProtoOAv1PnLChangeEvent(betterproto.Message):
1067
- """
1068
- The event that is sent when the unrealized PnL is changed due to market
1069
- movement. Requires subscribing to PnL events, see
1070
- ProtoOAv1PnLChangeSubscribeReq
1071
- """
1072
-
1073
- payload_type: "ProtoOAPayloadType" = betterproto.enum_field(1)
1074
- ctid_trader_account_id: int = betterproto.int64_field(2)
1075
- gross_unrealized_pn_l: int = betterproto.int64_field(3)
1076
- net_unrealized_pn_l: int = betterproto.int64_field(4)
1077
- money_digits: int = betterproto.uint32_field(5)
1078
-
1079
-
1080
- @dataclass
1081
- class ProtoOAv1PnLChangeSubscribeReq(betterproto.Message):
1082
- """The request to subscribe to ProtoOAv1PnLChangeEvent"""
1083
-
1084
- payload_type: "ProtoOAPayloadType" = betterproto.enum_field(1)
1085
- ctid_trader_account_id: int = betterproto.int64_field(2)
1086
-
1087
-
1088
- @dataclass
1089
- class ProtoOAv1PnLChangeSubscribeRes(betterproto.Message):
1090
- """The response for ProtoOAv1PnLChangeSubscribeReq"""
1091
-
1092
- payload_type: "ProtoOAPayloadType" = betterproto.enum_field(1)
1093
- ctid_trader_account_id: int = betterproto.int64_field(2)
1094
-
1095
-
1096
- @dataclass
1097
- class ProtoOAv1PnLChangeUnSubscribeReq(betterproto.Message):
1098
- """
1099
- The request to stop an existing subscription to PnL events. The subscriber
1100
- who sends this request will stop receiving ProtoOAv1PnLChangeEvent
1101
- """
1102
-
1103
- payload_type: "ProtoOAPayloadType" = betterproto.enum_field(1)
1104
- ctid_trader_account_id: int = betterproto.int64_field(2)
1105
-
1106
-
1107
- @dataclass
1108
- class ProtoOAv1PnLChangeUnSubscribeRes(betterproto.Message):
1109
- """The response for ProtoOAv1PnLChangeUnSubscribeReq"""
1110
-
1111
- payload_type: "ProtoOAPayloadType" = betterproto.enum_field(1)
1112
- ctid_trader_account_id: int = betterproto.int64_field(2)
@@ -96,11 +96,6 @@ class ProtoOAPayloadType(betterproto.Enum):
96
96
  PROTO_OA_DEAL_OFFSET_LIST_RES = 2186
97
97
  PROTO_OA_GET_POSITION_UNREALIZED_PNL_REQ = 2187
98
98
  PROTO_OA_GET_POSITION_UNREALIZED_PNL_RES = 2188
99
- PROTO_OA_V1_PNL_CHANGE_EVENT = 2189
100
- PROTO_OA_V1_PNL_CHANGE_SUBSCRIBE_REQ = 2190
101
- PROTO_OA_V1_PNL_CHANGE_SUBSCRIBE_RES = 2191
102
- PROTO_OA_V1_PNL_CHANGE_UN_SUBSCRIBE_REQ = 2192
103
- PROTO_OA_V1_PNL_CHANGE_UN_SUBSCRIBE_RES = 2193
104
99
 
105
100
 
106
101
  class ProtoOADayOfWeek(betterproto.Enum):
@@ -227,7 +222,7 @@ class ProtoOAOrderStatus(betterproto.Enum):
227
222
 
228
223
 
229
224
  class ProtoOAOrderTriggerMethod(betterproto.Enum):
230
- """* Stop Order and Stop Lost triggering method ENUM."""
225
+ """* Stop Order and Stop Loss triggering method ENUM."""
231
226
 
232
227
  TRADE = 1
233
228
  OPPOSITE = 2
@@ -406,7 +401,6 @@ class ProtoOAErrorCode(betterproto.Enum):
406
401
  UNABLE_TO_CANCEL_ORDER = 134
407
402
  UNABLE_TO_AMEND_ORDER = 135
408
403
  SHORT_SELLING_NOT_ALLOWED = 136
409
- NOT_SUBSCRIBED_TO_PNL = 137
410
404
 
411
405
 
412
406
  class ProtoOALimitedRiskMarginCalculationStrategy(betterproto.Enum):
@@ -97,11 +97,6 @@ from .OpenApiMessages import (
97
97
  ProtoOAUnsubscribeLiveTrendbarRes,
98
98
  ProtoOAUnsubscribeSpotsReq,
99
99
  ProtoOAUnsubscribeSpotsRes,
100
- ProtoOAv1PnLChangeEvent,
101
- ProtoOAv1PnLChangeSubscribeReq,
102
- ProtoOAv1PnLChangeSubscribeRes,
103
- ProtoOAv1PnLChangeUnSubscribeReq,
104
- ProtoOAv1PnLChangeUnSubscribeRes,
105
100
  ProtoOAVersionReq,
106
101
  ProtoOAVersionRes,
107
102
  )
@@ -311,10 +306,5 @@ __all__ = [
311
306
  "ProtoOAUnsubscribeSpotsRes",
312
307
  "ProtoOAVersionReq",
313
308
  "ProtoOAVersionRes",
314
- "ProtoOAv1PnLChangeEvent",
315
- "ProtoOAv1PnLChangeSubscribeReq",
316
- "ProtoOAv1PnLChangeSubscribeRes",
317
- "ProtoOAv1PnLChangeUnSubscribeReq",
318
- "ProtoOAv1PnLChangeUnSubscribeRes",
319
309
  "ProtoPayloadType",
320
310
  ]
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from collections.abc import Sequence
4
5
  from datetime import datetime
5
6
  from typing import TYPE_CHECKING
@@ -33,6 +34,9 @@ if TYPE_CHECKING:
33
34
  from ..connection import Protocol
34
35
 
35
36
 
37
+ logger = logging.getLogger(__name__)
38
+
39
+
36
40
  # Map TrendbarPeriod enum to proto values
37
41
  _PERIOD_TO_PROTO: dict[TrendbarPeriod, int] = {
38
42
  TrendbarPeriod.M1: ProtoOATrendbarPeriod.M1,
@@ -115,6 +119,7 @@ class MarketDataAPI:
115
119
  APIError: If request fails.
116
120
  CTraderConnectionTimeoutError: If request times out.
117
121
  """
122
+ logger.debug("Subscribing to spots: account=%d symbols=%s", account_id, symbol_ids)
118
123
  request = ProtoOASubscribeSpotsReq(
119
124
  ctid_trader_account_id=account_id,
120
125
  symbol_id=symbol_ids,
@@ -149,6 +154,7 @@ class MarketDataAPI:
149
154
  APIError: If request fails.
150
155
  CTraderConnectionTimeoutError: If request times out.
151
156
  """
157
+ logger.debug("Unsubscribing from spots: account=%d symbols=%s", account_id, symbol_ids)
152
158
  request = ProtoOAUnsubscribeSpotsReq(
153
159
  ctid_trader_account_id=account_id,
154
160
  symbol_id=symbol_ids,
@@ -193,6 +199,7 @@ class MarketDataAPI:
193
199
  APIError: If request fails.
194
200
  CTraderConnectionTimeoutError: If request times out.
195
201
  """
202
+ logger.debug("Subscribing to trendbars: account=%d symbol=%d period=%s", account_id, symbol_id, period.name)
196
203
  request = ProtoOASubscribeLiveTrendbarReq(
197
204
  ctid_trader_account_id=account_id,
198
205
  symbol_id=symbol_id,
@@ -229,6 +236,7 @@ class MarketDataAPI:
229
236
  APIError: If request fails.
230
237
  CTraderConnectionTimeoutError: If request times out.
231
238
  """
239
+ logger.debug("Unsubscribing from trendbars: account=%d symbol=%d period=%s", account_id, symbol_id, period.name)
232
240
  request = ProtoOAUnsubscribeLiveTrendbarReq(
233
241
  ctid_trader_account_id=account_id,
234
242
  symbol_id=symbol_id,
@@ -270,6 +278,7 @@ class MarketDataAPI:
270
278
  APIError: If request fails.
271
279
  CTraderConnectionTimeoutError: If request times out.
272
280
  """
281
+ logger.debug("Subscribing to depth: account=%d symbols=%s", account_id, symbol_ids)
273
282
  request = ProtoOASubscribeDepthQuotesReq(
274
283
  ctid_trader_account_id=account_id,
275
284
  symbol_id=symbol_ids,
@@ -303,6 +312,7 @@ class MarketDataAPI:
303
312
  APIError: If request fails.
304
313
  CTraderConnectionTimeoutError: If request times out.
305
314
  """
315
+ logger.debug("Unsubscribing from depth: account=%d symbols=%s", account_id, symbol_ids)
306
316
  request = ProtoOAUnsubscribeDepthQuotesReq(
307
317
  ctid_trader_account_id=account_id,
308
318
  symbol_id=symbol_ids,
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from datetime import UTC, datetime
4
5
  from decimal import Decimal
5
6
  from typing import TYPE_CHECKING
@@ -12,18 +13,18 @@ from .._internal.proto import (
12
13
  ProtoOADealListRes,
13
14
  ProtoOAExecutionEvent,
14
15
  ProtoOAExecutionType,
16
+ ProtoOAGetPositionUnrealizedPnLReq,
17
+ ProtoOAGetPositionUnrealizedPnLRes,
15
18
  ProtoOAOrderErrorEvent,
16
19
  ProtoOAOrderListReq,
17
20
  ProtoOAOrderListRes,
18
21
  ProtoOAReconcileReq,
19
22
  ProtoOAReconcileRes,
20
- ProtoOAv1PnLChangeSubscribeReq,
21
- ProtoOAv1PnLChangeSubscribeRes,
22
23
  )
23
24
  from ..enums import ExecutionType, OrderSide
24
25
  from ..events import ExecutionEvent
25
26
  from ..exceptions import APIError
26
- from ..models import Deal, Order, Position
27
+ from ..models import Deal, Order, Position, PositionUnrealizedPnL
27
28
  from ..models.requests import (
28
29
  AmendOrderRequest,
29
30
  AmendPositionRequest,
@@ -36,6 +37,9 @@ if TYPE_CHECKING:
36
37
  from ..connection import Protocol
37
38
 
38
39
 
40
+ logger = logging.getLogger(__name__)
41
+
42
+
39
43
  def _raise_if_order_error(response: object) -> None:
40
44
  """Raise APIError if response is an order error event.
41
45
 
@@ -164,31 +168,38 @@ class TradingAPI:
164
168
  self._protocol = protocol
165
169
  self._default_timeout = default_timeout
166
170
 
167
- async def subscribe_to_pnl_changes(self, account_id: int) -> None:
168
- """Subscribe to PnL change events.
169
-
170
- After subscribing, PnL change data will be delivered via the event system.
171
- Use `@client.on(PnLChangeEvent)` to handle them.
172
-
173
- Note:
174
- This subscription seems to be currently rate-limited by cTrader, so it may not work as expected.
171
+ async def get_unrealized_pnl_per_position(self, account_id: int) -> list[PositionUnrealizedPnL]:
172
+ """Get unrealized PnL for each open position.
175
173
 
176
174
  Args:
177
175
  account_id: The cTID trader account ID.
176
+
177
+ Returns:
178
+ List of PositionUnrealizedPnL, one per open position.
178
179
  """
179
- request = ProtoOAv1PnLChangeSubscribeReq(ctid_trader_account_id=account_id)
180
+ request = ProtoOAGetPositionUnrealizedPnLReq(ctid_trader_account_id=account_id)
180
181
 
181
182
  response = await self._protocol.send_request(
182
183
  request,
183
184
  timeout=self._default_timeout,
184
185
  )
185
186
 
186
- if not isinstance(response, ProtoOAv1PnLChangeSubscribeRes):
187
+ if not isinstance(response, ProtoOAGetPositionUnrealizedPnLRes):
187
188
  raise APIError(
188
189
  error_code="UNEXPECTED_RESPONSE",
189
- description=f"Expected ProtoOAv1PnLChangeSubscribeRes, got {type(response).__name__}",
190
+ description=f"Expected ProtoOAGetPositionUnrealizedPnLRes, got {type(response).__name__}",
190
191
  )
191
192
 
193
+ divisor = 10**response.money_digits
194
+ return [
195
+ PositionUnrealizedPnL(
196
+ position_id=p.position_id,
197
+ gross_unrealized_pnl=Decimal(p.gross_unrealized_pn_l) / divisor,
198
+ net_unrealized_pnl=Decimal(p.net_unrealized_pn_l) / divisor,
199
+ )
200
+ for p in response.position_unrealized_pn_l
201
+ ]
202
+
192
203
  async def place_order(
193
204
  self,
194
205
  account_id: int,
@@ -217,6 +228,13 @@ class TradingAPI:
217
228
  APIError: If request fails.
218
229
  CTraderConnectionTimeoutError: If request times out.
219
230
  """
231
+ logger.debug(
232
+ "Placing order: account=%d symbol=%d type=%s volume=%d",
233
+ account_id,
234
+ request.symbol_id,
235
+ request.order_type.name,
236
+ request.volume,
237
+ )
220
238
  proto_request = request.to_proto(account_id)
221
239
 
222
240
  response = await self._protocol.send_request(
@@ -254,6 +272,7 @@ class TradingAPI:
254
272
  APIError: If request fails or order not found.
255
273
  CTraderConnectionTimeoutError: If request times out.
256
274
  """
275
+ logger.debug("Amending order: account=%d order=%d", account_id, request.order_id)
257
276
  proto_request = request.to_proto(account_id)
258
277
 
259
278
  response = await self._protocol.send_request(
@@ -291,6 +310,7 @@ class TradingAPI:
291
310
  APIError: If request fails or order not found.
292
311
  CTraderConnectionTimeoutError: If request times out.
293
312
  """
313
+ logger.debug("Cancelling order: account=%d order=%d", account_id, order_id)
294
314
  request = ProtoOACancelOrderReq(
295
315
  ctid_trader_account_id=account_id,
296
316
  order_id=order_id,
@@ -331,6 +351,7 @@ class TradingAPI:
331
351
  APIError: If request fails or position not found.
332
352
  CTraderConnectionTimeoutError: If request times out.
333
353
  """
354
+ logger.debug("Closing position: account=%d position=%d", account_id, request.position_id)
334
355
  proto_request = request.to_proto(account_id)
335
356
 
336
357
  response = await self._protocol.send_request(
@@ -368,6 +389,7 @@ class TradingAPI:
368
389
  APIError: If request fails or position not found.
369
390
  CTraderConnectionTimeoutError: If request times out.
370
391
  """
392
+ logger.debug("Amending position: account=%d position=%d", account_id, request.position_id)
371
393
  proto_request = request.to_proto(account_id)
372
394
 
373
395
  response = await self._protocol.send_request(