ctrader-api-client 0.2.3__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.3 → ctrader_api_client-0.3.0}/Justfile +4 -0
  2. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/PKG-INFO +1 -1
  3. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/docs/api/accounts.md +0 -1
  4. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/docs/api/events.md +0 -4
  5. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/docs/api/models.md +3 -0
  6. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/docs/api/trading.md +10 -0
  7. ctrader_api_client-0.3.0/protos/VERSION +1 -0
  8. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/protos/vendor/OpenApiMessages.proto +5 -38
  9. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/protos/vendor/OpenApiModelMessages.proto +1 -7
  10. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/pyproject.toml +1 -1
  11. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/messages.py +0 -10
  12. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +1 -51
  13. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +1 -7
  14. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -10
  15. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/api/market_data.py +10 -0
  16. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/api/trading.py +36 -14
  17. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/client.py +0 -2
  18. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/connection/heartbeat.py +3 -0
  19. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/connection/protocol.py +8 -3
  20. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/connection/transport.py +16 -2
  21. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/events/__init__.py +0 -2
  22. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/events/router.py +17 -19
  23. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/events/types.py +0 -21
  24. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/__init__.py +2 -1
  25. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/position.py +15 -0
  26. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/api/test_trading.py +85 -1
  27. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/events/test_router.py +0 -31
  28. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/events/test_types.py +0 -19
  29. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/uv.lock +1 -1
  30. ctrader_api_client-0.2.3/protos/VERSION +0 -1
  31. ctrader_api_client-0.2.3/protos/vendor/.gitkeep +0 -0
  32. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/.claude/settings.local.json +0 -0
  33. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/.github/workflows/docs.yml +0 -0
  34. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/.gitignore +0 -0
  35. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/.pre-commit-config.yaml +0 -0
  36. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/.python-version +0 -0
  37. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/LICENSE +0 -0
  38. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/README.md +0 -0
  39. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/docs/api/client.md +0 -0
  40. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/docs/api/enums.md +0 -0
  41. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/docs/api/market-data.md +0 -0
  42. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/docs/api/symbols.md +0 -0
  43. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/docs/getting-started.md +0 -0
  44. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/docs/index.md +0 -0
  45. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/mkdocs.yml +0 -0
  46. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/protos/SOURCE +0 -0
  47. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/protos/update.sh +0 -0
  48. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
  49. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
  50. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/scripts/fix_proto_imports.py +0 -0
  51. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/__init__.py +0 -0
  52. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/__init__.py +0 -0
  53. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
  54. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
  55. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
  56. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/api/__init__.py +0 -0
  57. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/api/accounts.py +0 -0
  58. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/api/symbols.py +0 -0
  59. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/auth/__init__.py +0 -0
  60. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/auth/credentials.py +0 -0
  61. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/auth/manager.py +0 -0
  62. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/config.py +0 -0
  63. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/connection/__init__.py +0 -0
  64. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/enums.py +0 -0
  65. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/events/emitter.py +0 -0
  66. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/exceptions.py +0 -0
  67. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/_base.py +0 -0
  68. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/account.py +0 -0
  69. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/deal.py +0 -0
  70. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/market_data.py +0 -0
  71. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/order.py +0 -0
  72. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/requests.py +0 -0
  73. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/models/symbol.py +0 -0
  74. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/src/ctrader_api_client/py.typed +0 -0
  75. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/_internal/test_messages.py +0 -0
  76. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/_internal/test_serialization.py +0 -0
  77. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/api/conftest.py +0 -0
  78. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/api/test_accounts.py +0 -0
  79. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/api/test_market_data_api.py +0 -0
  80. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/api/test_symbols.py +0 -0
  81. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/auth/test_credentials.py +0 -0
  82. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/auth/test_manager.py +0 -0
  83. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/connection/test_heartbeat.py +0 -0
  84. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/connection/test_protocol.py +0 -0
  85. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/connection/test_transport.py +0 -0
  86. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/events/test_emitter.py +0 -0
  87. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/models/test_account.py +0 -0
  88. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/models/test_deal.py +0 -0
  89. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/models/test_market_data.py +0 -0
  90. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/models/test_order.py +0 -0
  91. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/models/test_position.py +0 -0
  92. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/models/test_requests.py +0 -0
  93. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.0}/tests/unit/models/test_symbol.py +0 -0
  94. {ctrader_api_client-0.2.3 → ctrader_api_client-0.3.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.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.3"
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
@@ -56,6 +56,7 @@ class HeartbeatManager:
56
56
  self._task_group = anyio.create_task_group()
57
57
  await self._task_group.__aenter__()
58
58
  self._task_group.start_soon(self._heartbeat_loop)
59
+ logger.info("Heartbeat monitor started (interval=%.1fs, timeout=%.1fs)", self._interval, self._timeout)
59
60
 
60
61
  async def stop(self) -> None:
61
62
  """Stop heartbeat monitoring.
@@ -66,6 +67,7 @@ class HeartbeatManager:
66
67
  self._task_scope.cancel()
67
68
 
68
69
  if self._task_group is not None:
70
+ logger.info("Heartbeat monitor stopped")
69
71
  self._task_group.cancel_scope.cancel()
70
72
  try:
71
73
  await self._task_group.__aexit__(None, None, None)
@@ -85,6 +87,7 @@ class HeartbeatManager:
85
87
  self._last_received = time.monotonic()
86
88
  if self._task_group is not None:
87
89
  self._task_group.start_soon(self._heartbeat_loop)
90
+ logger.info("Heartbeat monitor restarted")
88
91
 
89
92
  async def _record_activity(self, _message: betterproto.Message) -> None:
90
93
  """Reset the inactivity timer on any received server message."""
@@ -235,11 +235,16 @@ class Protocol:
235
235
  proto_msg = deserialize_proto_message(raw)
236
236
  inner = unwrap_message(proto_msg)
237
237
  await self._dispatch_message(proto_msg, inner)
238
- except (FramingError, anyio.ClosedResourceError, anyio.EndOfStream):
239
- # Connection closed or corrupted
238
+ except FramingError as e:
239
+ logger.error("Protocol framing error (possible data corruption): %s", e)
240
240
  if self._running:
241
241
  await self.handle_disconnect()
242
242
  break
243
+ except (anyio.ClosedResourceError, anyio.EndOfStream):
244
+ if self._running:
245
+ logger.debug("Connection closed by remote")
246
+ await self.handle_disconnect()
247
+ break
243
248
  except anyio.get_cancelled_exc_class():
244
249
  break
245
250
  except Exception as e:
@@ -310,7 +315,7 @@ class Protocol:
310
315
 
311
316
  async def handle_disconnect(self) -> None:
312
317
  """Handle unexpected disconnection and attempt reconnection."""
313
- logger.info("Connection lost, attempting to reconnect...")
318
+ logger.warning("Connection lost, attempting to reconnect...")
314
319
 
315
320
  # Cancel the reader loop first to prevent race conditions
316
321
  if self._reader_scope is not None:
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import ssl
4
5
 
5
6
  import anyio
@@ -8,6 +9,9 @@ from anyio.abc import ByteReceiveStream, ByteStream
8
9
  from ..exceptions import CTraderConnectionClosedError, CTraderConnectionFailedError
9
10
 
10
11
 
12
+ logger = logging.getLogger(__name__)
13
+
14
+
11
15
  class Transport:
12
16
  """Low-level TCP/SSL transport.
13
17
 
@@ -78,6 +82,11 @@ class Transport:
78
82
  except OSError as e:
79
83
  raise CTraderConnectionFailedError(self._host, self._port, e) from e
80
84
 
85
+ if self._ssl:
86
+ logger.info("Connected to %s:%d with SSL", self._host, self._port)
87
+ else:
88
+ logger.warning("Connected to %s:%d without SSL (plaintext)", self._host, self._port)
89
+
81
90
  async def close(self) -> None:
82
91
  """Close the connection gracefully.
83
92
 
@@ -88,9 +97,14 @@ class Transport:
88
97
  stream = self._stream
89
98
  self._stream = None # Clear reference first to prevent re-entry
90
99
  try:
91
- await stream.aclose()
100
+ # move_on_after guards against aclose() hanging when the network
101
+ # route has changed (e.g. VPN toggle) but the OS hasn't reset the
102
+ # TCP socket — graceful TLS shutdown would wait forever for an ACK.
103
+ with anyio.move_on_after(5) as close_scope:
104
+ await stream.aclose()
105
+ if close_scope.cancelled_caught:
106
+ logger.debug("Graceful TLS shutdown timed out, forcing close")
92
107
  except (OSError, anyio.ClosedResourceError):
93
- # Stream already closed or in bad state - ignore
94
108
  pass
95
109
 
96
110
  async def send(self, data: bytes) -> None:
@@ -31,7 +31,6 @@ from .types import (
31
31
  MarginCallTriggerEvent,
32
32
  MarginChangeEvent,
33
33
  OrderErrorEvent,
34
- PnLChangeEvent,
35
34
  ReadyEvent,
36
35
  ReconnectedEvent,
37
36
  SpotEvent,
@@ -54,7 +53,6 @@ __all__ = [
54
53
  "MarginCallTriggerEvent",
55
54
  "MarginChangeEvent",
56
55
  "OrderErrorEvent",
57
- "PnLChangeEvent",
58
56
  "ReadyEvent",
59
57
  "ReconnectedEvent",
60
58
  "SpotEvent",