ctrader-api-client 0.2.2__tar.gz → 0.3.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.2 → ctrader_api_client-0.3.0}/.pre-commit-config.yaml +8 -0
  2. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/Justfile +5 -0
  3. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/PKG-INFO +1 -1
  4. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/docs/api/accounts.md +0 -1
  5. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/docs/api/events.md +0 -4
  6. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/docs/api/models.md +3 -0
  7. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/docs/api/trading.md +10 -0
  8. ctrader_api_client-0.3.0/protos/VERSION +1 -0
  9. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/protos/vendor/OpenApiMessages.proto +5 -38
  10. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/protos/vendor/OpenApiModelMessages.proto +1 -7
  11. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/pyproject.toml +1 -1
  12. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/messages.py +0 -10
  13. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +1 -51
  14. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +1 -7
  15. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -10
  16. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/api/market_data.py +10 -0
  17. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/api/trading.py +36 -14
  18. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/client.py +0 -2
  19. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/config.py +2 -2
  20. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/connection/heartbeat.py +15 -10
  21. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/connection/protocol.py +18 -10
  22. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/connection/transport.py +16 -2
  23. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/events/__init__.py +0 -2
  24. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/events/router.py +17 -19
  25. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/events/types.py +0 -21
  26. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/__init__.py +2 -1
  27. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/market_data.py +1 -1
  28. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/position.py +15 -0
  29. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/api/test_trading.py +85 -1
  30. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/connection/test_heartbeat.py +40 -14
  31. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/connection/test_protocol.py +23 -0
  32. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/events/test_router.py +0 -31
  33. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/events/test_types.py +0 -19
  34. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/models/test_market_data.py +2 -2
  35. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/uv.lock +1 -1
  36. ctrader_api_client-0.2.2/protos/VERSION +0 -1
  37. ctrader_api_client-0.2.2/protos/vendor/.gitkeep +0 -0
  38. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/.claude/settings.local.json +0 -0
  39. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/.github/workflows/docs.yml +0 -0
  40. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/.gitignore +0 -0
  41. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/.python-version +0 -0
  42. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/LICENSE +0 -0
  43. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/README.md +0 -0
  44. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/docs/api/client.md +0 -0
  45. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/docs/api/enums.md +0 -0
  46. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/docs/api/market-data.md +0 -0
  47. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/docs/api/symbols.md +0 -0
  48. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/docs/getting-started.md +0 -0
  49. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/docs/index.md +0 -0
  50. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/mkdocs.yml +0 -0
  51. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/protos/SOURCE +0 -0
  52. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/protos/update.sh +0 -0
  53. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
  54. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
  55. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/scripts/fix_proto_imports.py +0 -0
  56. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/__init__.py +0 -0
  57. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/__init__.py +0 -0
  58. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
  59. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
  60. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
  61. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/api/__init__.py +0 -0
  62. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/api/accounts.py +0 -0
  63. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/api/symbols.py +0 -0
  64. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/auth/__init__.py +0 -0
  65. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/auth/credentials.py +0 -0
  66. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/auth/manager.py +0 -0
  67. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/connection/__init__.py +0 -0
  68. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/enums.py +0 -0
  69. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/events/emitter.py +0 -0
  70. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/exceptions.py +0 -0
  71. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/_base.py +0 -0
  72. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/account.py +0 -0
  73. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/deal.py +0 -0
  74. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/order.py +0 -0
  75. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/requests.py +0 -0
  76. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/symbol.py +0 -0
  77. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/src/ctrader_api_client/py.typed +0 -0
  78. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/_internal/test_messages.py +0 -0
  79. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/_internal/test_serialization.py +0 -0
  80. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/api/conftest.py +0 -0
  81. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/api/test_accounts.py +0 -0
  82. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/api/test_market_data_api.py +0 -0
  83. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/api/test_symbols.py +0 -0
  84. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/auth/test_credentials.py +0 -0
  85. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/auth/test_manager.py +0 -0
  86. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/connection/test_transport.py +0 -0
  87. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/events/test_emitter.py +0 -0
  88. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/models/test_account.py +0 -0
  89. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/models/test_deal.py +0 -0
  90. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/models/test_order.py +0 -0
  91. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/models/test_position.py +0 -0
  92. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/models/test_requests.py +0 -0
  93. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/models/test_symbol.py +0 -0
  94. {ctrader_api_client-0.2.2 → ctrader_api_client-0.3.0}/tests/unit/test_client.py +0 -0
@@ -1,4 +1,12 @@
1
1
  repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: local-ci
5
+ name: Local CI Checks
6
+ entry: just ci
7
+ language: system
8
+ pass_filenames: false
9
+
2
10
  # Ruff for linting & formatting
3
11
  - repo: https://github.com/astral-sh/ruff-pre-commit
4
12
  rev: v0.12.9
@@ -10,11 +10,16 @@ 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}}
16
20
  @just fmt {{directory}}
17
21
  @just type-check {{directory}}
22
+ @just test {{directory}}
18
23
 
19
24
  # Lint the codebase using ruff, optionally specifying a directory to lint.
20
25
  lint directory='':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctrader-api-client
3
- Version: 0.2.2
3
+ Version: 0.3.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
@@ -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
@@ -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:
@@ -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
+ ```
@@ -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.2"
3
+ version = "0.3.0"
4
4
  description = "API Client to interact with the cTrader Open API spec"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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=p.gross_unrealized_pn_l / divisor,
198
+ net_unrealized_pnl=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(
@@ -19,7 +19,6 @@ from .events import (
19
19
  MarginCallTriggerEvent,
20
20
  MarginChangeEvent,
21
21
  OrderErrorEvent,
22
- PnLChangeEvent,
23
22
  ReadyEvent,
24
23
  ReconnectedEvent,
25
24
  SpotEvent,
@@ -50,7 +49,6 @@ T_AccountIdOnly = TypeVar(
50
49
  SymbolChangedEvent,
51
50
  TrailingStopChangedEvent,
52
51
  MarginCallTriggerEvent,
53
- PnLChangeEvent,
54
52
  )
55
53
 
56
54
  # Events that support no filters
@@ -13,7 +13,7 @@ class ClientConfig(BaseModel):
13
13
  client_id: OAuth application client ID.
14
14
  client_secret: OAuth application client secret.
15
15
  heartbeat_interval: Seconds between heartbeat sends.
16
- heartbeat_timeout: Seconds without server heartbeat before disconnect. Set to 0 to disable.
16
+ heartbeat_timeout: Seconds without server-initiated messages before disconnect. Set to 0 to disable.
17
17
  request_timeout: Default timeout for API requests in seconds.
18
18
  reconnect_attempts: Max reconnection attempts (0 to disable).
19
19
  reconnect_min_wait: Initial wait between reconnection attempts.
@@ -43,7 +43,7 @@ class ClientConfig(BaseModel):
43
43
 
44
44
  # Heartbeat settings
45
45
  heartbeat_interval: float = Field(default=10.0, gt=0)
46
- heartbeat_timeout: float = Field(default=0, ge=0)
46
+ heartbeat_timeout: float = Field(default=60.0, ge=0)
47
47
 
48
48
  # Request settings
49
49
  request_timeout: float = Field(default=30.0, gt=0)
@@ -5,6 +5,7 @@ import time
5
5
 
6
6
  import anyio
7
7
  import anyio.abc
8
+ import betterproto
8
9
 
9
10
  from .._internal.proto import ProtoHeartbeatEvent
10
11
  from .protocol import Protocol
@@ -43,10 +44,11 @@ class HeartbeatManager:
43
44
  async def start(self) -> None:
44
45
  """Start heartbeat monitoring.
45
46
 
46
- Registers an event handler for incoming heartbeats and starts
47
- the heartbeat send loop.
47
+ Registers event handlers and starts the heartbeat send loop.
48
48
  """
49
- # Register handler for incoming heartbeats
49
+ # Track activity on any server message, not just heartbeats
50
+ self._protocol.on_event(betterproto.Message, self._record_activity)
51
+ # Keep heartbeat handler for debug logging
50
52
  self._protocol.on_event(ProtoHeartbeatEvent, self._on_heartbeat)
51
53
  self._last_received = time.monotonic()
52
54
 
@@ -54,16 +56,18 @@ class HeartbeatManager:
54
56
  self._task_group = anyio.create_task_group()
55
57
  await self._task_group.__aenter__()
56
58
  self._task_group.start_soon(self._heartbeat_loop)
59
+ logger.info("Heartbeat monitor started (interval=%.1fs, timeout=%.1fs)", self._interval, self._timeout)
57
60
 
58
61
  async def stop(self) -> None:
59
62
  """Stop heartbeat monitoring.
60
63
 
61
- Cancels the heartbeat loop and removes the event handler.
64
+ Cancels the heartbeat loop and removes event handlers.
62
65
  """
63
66
  if self._task_scope is not None:
64
67
  self._task_scope.cancel()
65
68
 
66
69
  if self._task_group is not None:
70
+ logger.info("Heartbeat monitor stopped")
67
71
  self._task_group.cancel_scope.cancel()
68
72
  try:
69
73
  await self._task_group.__aexit__(None, None, None)
@@ -71,6 +75,7 @@ class HeartbeatManager:
71
75
  pass
72
76
  self._task_group = None
73
77
 
78
+ self._protocol.remove_handler(betterproto.Message, self._record_activity)
74
79
  self._protocol.remove_handler(ProtoHeartbeatEvent, self._on_heartbeat)
75
80
 
76
81
  async def restart(self) -> None:
@@ -82,14 +87,14 @@ class HeartbeatManager:
82
87
  self._last_received = time.monotonic()
83
88
  if self._task_group is not None:
84
89
  self._task_group.start_soon(self._heartbeat_loop)
90
+ logger.info("Heartbeat monitor restarted")
85
91
 
86
- async def _on_heartbeat(self, _event: ProtoHeartbeatEvent) -> None:
87
- """Handler called when heartbeat received from server.
88
-
89
- Args:
90
- _event: The heartbeat event from the server (unused).
91
- """
92
+ async def _record_activity(self, _message: betterproto.Message) -> None:
93
+ """Reset the inactivity timer on any received server message."""
92
94
  self._last_received = time.monotonic()
95
+
96
+ async def _on_heartbeat(self, _event: ProtoHeartbeatEvent) -> None:
97
+ """Handler called when an explicit heartbeat is received from the server."""
93
98
  logger.debug("Heartbeat received from server")
94
99
 
95
100
  async def _heartbeat_loop(self) -> None: