ctrader-api-client 0.8.1__tar.gz → 0.9.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.

Potentially problematic release.


This version of ctrader-api-client might be problematic. Click here for more details.

Files changed (127) hide show
  1. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/PKG-INFO +5 -1
  2. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/README.md +4 -0
  3. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/docs/api/enums.md +10 -0
  4. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/docs/api/models.md +60 -0
  5. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/docs/api/symbols.md +29 -0
  6. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/pyproject.toml +1 -1
  7. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/__init__.py +6 -0
  8. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/enums.py +29 -0
  9. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/__init__.py +2 -0
  10. ctrader_api_client-0.9.0/src/ctrader_api_client/models/commission.py +148 -0
  11. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/symbol.py +6 -4
  12. ctrader_api_client-0.9.0/tests/unit/test_commission_conversion.py +187 -0
  13. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/uv.lock +1 -1
  14. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/.claude/settings.local.json +0 -0
  15. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/.github/workflows/docs.yml +0 -0
  16. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/.gitignore +0 -0
  17. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/.pre-commit-config.yaml +0 -0
  18. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/.python-version +0 -0
  19. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/Justfile +0 -0
  20. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/LICENSE +0 -0
  21. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/docs/api/accounts.md +0 -0
  22. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/docs/api/client.md +0 -0
  23. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/docs/api/events.md +0 -0
  24. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/docs/api/exceptions.md +0 -0
  25. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/docs/api/market-data.md +0 -0
  26. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/docs/api/trading.md +0 -0
  27. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/docs/getting-started.md +0 -0
  28. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/docs/index.md +0 -0
  29. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/mkdocs.yml +0 -0
  30. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/protos/SOURCE +0 -0
  31. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/protos/VERSION +0 -0
  32. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/protos/update.sh +0 -0
  33. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
  34. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
  35. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/protos/vendor/OpenApiMessages.proto +0 -0
  36. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/protos/vendor/OpenApiModelMessages.proto +0 -0
  37. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/scripts/fix_proto_imports.py +0 -0
  38. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/__init__.py +0 -0
  39. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/clock.py +0 -0
  40. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/conversions.py +0 -0
  41. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/messages.py +0 -0
  42. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
  43. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
  44. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
  45. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
  46. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
  47. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
  48. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/__init__.py +0 -0
  49. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/_base.py +0 -0
  50. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/accounts.py +0 -0
  51. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/market_data.py +0 -0
  52. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/symbols.py +0 -0
  53. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/trading.py +0 -0
  54. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/__init__.py +0 -0
  55. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/_recovery.py +0 -0
  56. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/_refresh.py +0 -0
  57. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/_session.py +0 -0
  58. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/credentials.py +0 -0
  59. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/manager.py +0 -0
  60. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/policy.py +0 -0
  61. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/store.py +0 -0
  62. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/client.py +0 -0
  63. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/composition.py +0 -0
  64. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/config.py +0 -0
  65. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/__init__.py +0 -0
  66. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/heartbeat.py +0 -0
  67. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/listener.py +0 -0
  68. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/protocol.py +0 -0
  69. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/supervisor.py +0 -0
  70. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/transport.py +0 -0
  71. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/__init__.py +0 -0
  72. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/_execution.py +0 -0
  73. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/emitter.py +0 -0
  74. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/publisher.py +0 -0
  75. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/router.py +0 -0
  76. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/types.py +0 -0
  77. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/exceptions.py +0 -0
  78. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/_base.py +0 -0
  79. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/account.py +0 -0
  80. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/deal.py +0 -0
  81. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/market_data.py +0 -0
  82. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/order.py +0 -0
  83. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/position.py +0 -0
  84. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/requests.py +0 -0
  85. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/trading_hours.py +0 -0
  86. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/src/ctrader_api_client/py.typed +0 -0
  87. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/__init__.py +0 -0
  88. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/__init__.py +0 -0
  89. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/api/__init__.py +0 -0
  90. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/api/conftest.py +0 -0
  91. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/api/test_market_data.py +0 -0
  92. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/api/test_reference_data.py +0 -0
  93. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/api/test_subscription_restore.py +0 -0
  94. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/api/test_trading.py +0 -0
  95. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/auth/__init__.py +0 -0
  96. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/auth/conftest.py +0 -0
  97. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/auth/test_authentication.py +0 -0
  98. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/auth/test_token_lifecycle.py +0 -0
  99. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/connection/__init__.py +0 -0
  100. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/connection/conftest.py +0 -0
  101. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/connection/test_event_dispatch.py +0 -0
  102. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/connection/test_framing.py +0 -0
  103. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/connection/test_heartbeat.py +0 -0
  104. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/connection/test_reconnection.py +0 -0
  105. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/connection/test_request_response.py +0 -0
  106. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/events/__init__.py +0 -0
  107. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/events/conftest.py +0 -0
  108. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/events/test_routing.py +0 -0
  109. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/events/test_subscriptions.py +0 -0
  110. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/behavior/test_client.py +0 -0
  111. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/conftest.py +0 -0
  112. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/harness/__init__.py +0 -0
  113. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/harness/clock.py +0 -0
  114. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/harness/factories.py +0 -0
  115. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/harness/recorder.py +0 -0
  116. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/harness/server.py +0 -0
  117. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/harness/signals.py +0 -0
  118. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/harness/stub_protocol.py +0 -0
  119. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/harness/wire.py +0 -0
  120. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/unit/__init__.py +0 -0
  121. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/unit/test_api_errors.py +0 -0
  122. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/unit/test_market_data_conversion.py +0 -0
  123. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/unit/test_request_conversion.py +0 -0
  124. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/unit/test_symbol_conversion.py +0 -0
  125. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/unit/test_trading_hours.py +0 -0
  126. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/unit/test_trading_records.py +0 -0
  127. {ctrader_api_client-0.8.1 → ctrader_api_client-0.9.0}/tests/unit/test_wire_format.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ctrader-api-client
3
- Version: 0.8.1
3
+ Version: 0.9.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
@@ -303,6 +303,10 @@ results = await client.symbols.search(account_id, "EUR")
303
303
  # Get specific symbol
304
304
  symbol = await client.symbols.get_by_id(account_id, symbol_id)
305
305
 
306
+ # The commission rate comes with the type that says what the rate counts
307
+ commission = symbol.commission
308
+ print(f"{commission.rate} ({commission.type.value}), min {commission.minimum}")
309
+
306
310
  # Trading hours arrive with the symbol, so this costs no extra request
307
311
  hours = symbol.trading_hours
308
312
  if hours.is_open():
@@ -283,6 +283,10 @@ results = await client.symbols.search(account_id, "EUR")
283
283
  # Get specific symbol
284
284
  symbol = await client.symbols.get_by_id(account_id, symbol_id)
285
285
 
286
+ # The commission rate comes with the type that says what the rate counts
287
+ commission = symbol.commission
288
+ print(f"{commission.rate} ({commission.type.value}), min {commission.minimum}")
289
+
286
290
  # Trading hours arrive with the symbol, so this costs no extra request
287
291
  hours = symbol.trading_hours
288
292
  if hours.is_open():
@@ -69,6 +69,16 @@ from ctrader_api_client import ExecutionType, OrderSide, OrderType
69
69
  show_source: false
70
70
  members: true
71
71
 
72
+ ::: ctrader_api_client.enums.CommissionType
73
+ options:
74
+ show_source: false
75
+ members: true
76
+
77
+ ::: ctrader_api_client.enums.MinCommissionType
78
+ options:
79
+ show_source: false
80
+ members: true
81
+
72
82
  ## Market Data Enums
73
83
 
74
84
  ::: ctrader_api_client.enums.TrendbarPeriod
@@ -130,6 +130,66 @@ partial_close = ClosePositionRequest(
130
130
  show_source: false
131
131
 
132
132
 
133
+ ::: ctrader_api_client.models.Commission
134
+ options:
135
+ show_source: false
136
+
137
+ **Example:**
138
+
139
+ A broker states a commission as a rate and a type. The type says what the rate counts, so
140
+ you must read the two together. The rate alone is meaningless: `2.5` is 2.50 USD per lot
141
+ under one type and 2.5% of the traded value under another.
142
+
143
+ ```python
144
+ from decimal import Decimal
145
+
146
+ from ctrader_api_client import CommissionType, Symbol
147
+
148
+
149
+ def commission_per_side(symbol: Symbol, lots: Decimal, price: Decimal) -> Decimal:
150
+ """Commission for one side of a trade.
151
+
152
+ The result is in USD for the two USD types, and in the symbol's quote currency
153
+ for QUOTE_CCY_PER_LOT.
154
+ """
155
+ # lot_size counts hundredths of a base unit, so 100 gives the contract size.
156
+ notional = lots * symbol.lot_size / 100 * price
157
+
158
+ match symbol.commission.type:
159
+ case CommissionType.USD_PER_MILLION_USD:
160
+ return symbol.commission.rate * notional / 1_000_000
161
+ case CommissionType.USD_PER_LOT | CommissionType.QUOTE_CCY_PER_LOT:
162
+ return symbol.commission.rate * lots
163
+ case CommissionType.PERCENTAGE_OF_VALUE:
164
+ return symbol.commission.rate / 100 * notional
165
+
166
+
167
+ # Symbol IDs belong to the broker's server, so look yours up with symbols.search().
168
+ gold = await client.symbols.get_by_id(account_id, 41)
169
+
170
+ # 0.0007 (PERCENTAGE_OF_VALUE), a lot of 100 ounces
171
+ print(gold.commission.rate, gold.commission.type.value)
172
+
173
+ # 3.07689900 USD for 1 lot at 4395.57, which the broker charges as 3.08
174
+ print(commission_per_side(gold, Decimal(1), Decimal("4395.57")))
175
+ ```
176
+
177
+ These rates come from one live broker, and show how far the types differ:
178
+
179
+ | Symbol | Type | Rate | Charge per side, for one lot |
180
+ |--------|------|------|------------------------------|
181
+ | EURUSD | `USD_PER_LOT` | 2.5 | 2.50 USD, at any price |
182
+ | XAUUSD | `PERCENTAGE_OF_VALUE` | 0.0007 | 3.08 USD, at a price of 4395.57 |
183
+ | BTCUSD | `PERCENTAGE_OF_VALUE` | 0.0325 | 20.89 USD, at a price of 64286.15 |
184
+
185
+ The broker charges the commission per side, so a round turn costs twice as much.
186
+
187
+ `minimum` is the smallest commission the broker charges for one trade. Apply it only when
188
+ its currency matches your charge: `minimum_type` tells you whether `minimum` is in the
189
+ symbol's quote currency, or in the currency that `minimum_asset` names. A `minimum` of 0
190
+ means the broker sets no minimum.
191
+
192
+
133
193
  ::: ctrader_api_client.models.TradingHours
134
194
  options:
135
195
  show_source: false
@@ -58,6 +58,35 @@ for sym in eur_pairs:
58
58
  print(sym.name)
59
59
  ```
60
60
 
61
+ ## Commission
62
+
63
+ A broker states a commission as a rate and a type. The type says what the rate counts, so
64
+ you must read the two together. The same rate of `35` is 35 USD per million USD of volume
65
+ for one symbol, and 35 USD per lot for another.
66
+
67
+ ```python
68
+ from ctrader_api_client import CommissionType
69
+
70
+ symbol = await client.symbols.get_by_id(account_id, 270)
71
+ commission = symbol.commission
72
+
73
+ print(f"{commission.rate} ({commission.type.value})")
74
+ print(f"Minimum: {commission.minimum} {commission.minimum_asset}")
75
+ ```
76
+
77
+ | Type | Meaning of the rate |
78
+ |------|---------------------|
79
+ | `USD_PER_MILLION_USD` | USD per million USD of traded volume. Usual for forex. |
80
+ | `USD_PER_LOT` | USD per lot. Usual for commodity and index CFDs and futures. |
81
+ | `PERCENTAGE_OF_VALUE` | Percent of the traded value. Usual for equities. |
82
+ | `QUOTE_CCY_PER_LOT` | The symbol's quote currency per lot. |
83
+
84
+ The broker charges the commission per side, so a round turn costs twice the rate. The
85
+ charge lands in USD for the first two types, and in the symbol's quote currency for
86
+ `QUOTE_CCY_PER_LOT`. Convert to your deposit currency yourself. See
87
+ [Models](models.md#ctrader_api_client.models.Commission) for the arithmetic of each type,
88
+ and for how to apply `minimum`.
89
+
61
90
  ## Trading Hours
62
91
 
63
92
  Every `Symbol` arrives with its trading calendar attached, so asking when a symbol is open
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctrader-api-client"
3
- version = "0.8.1"
3
+ version = "0.9.0"
4
4
  description = "API Client to interact with the cTrader Open API spec"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -50,8 +50,10 @@ from .enums import (
50
50
  AccessRights,
51
51
  AccountType,
52
52
  AuthTrigger,
53
+ CommissionType,
53
54
  DealStatus,
54
55
  ExecutionType,
56
+ MinCommissionType,
55
57
  OrderSide,
56
58
  OrderStatus,
57
59
  OrderType,
@@ -108,6 +110,7 @@ from .models import (
108
110
  AmendPositionRequest,
109
111
  CloseDetail,
110
112
  ClosePositionRequest,
113
+ Commission,
111
114
  Deal,
112
115
  Holiday,
113
116
  NewOrderRequest,
@@ -150,6 +153,8 @@ __all__ = [
150
153
  "ClientDisconnectEvent",
151
154
  "CloseDetail",
152
155
  "ClosePositionRequest",
156
+ "Commission",
157
+ "CommissionType",
153
158
  "Deal",
154
159
  "DealStatus",
155
160
  "DepthEvent",
@@ -162,6 +167,7 @@ __all__ = [
162
167
  "Holiday",
163
168
  "MarginCallTriggerEvent",
164
169
  "MarginChangeEvent",
170
+ "MinCommissionType",
165
171
  "NewOrderRequest",
166
172
  "Order",
167
173
  "OrderErrorEvent",
@@ -117,6 +117,35 @@ class TradingMode(Enum):
117
117
  CLOSE_ONLY = "CLOSE_ONLY"
118
118
 
119
119
 
120
+ class CommissionType(Enum):
121
+ """Unit a symbol's commission rate is expressed in.
122
+
123
+ The rate is a plain number. This type tells you what the number counts.
124
+ """
125
+
126
+ USD_PER_MILLION_USD = "USD_PER_MILLION_USD"
127
+ """USD per million USD of traded volume. Usual for forex."""
128
+
129
+ USD_PER_LOT = "USD_PER_LOT"
130
+ """USD per lot. Usual for commodity and index CFDs and futures."""
131
+
132
+ PERCENTAGE_OF_VALUE = "PERCENTAGE_OF_VALUE"
133
+ """Percent of the traded value. Usual for equities."""
134
+
135
+ QUOTE_CCY_PER_LOT = "QUOTE_CCY_PER_LOT"
136
+ """The symbol's quote currency per lot."""
137
+
138
+
139
+ class MinCommissionType(Enum):
140
+ """Currency a symbol's minimum commission is expressed in."""
141
+
142
+ CURRENCY = "CURRENCY"
143
+ """The currency named by the minimum commission asset."""
144
+
145
+ QUOTE_CURRENCY = "QUOTE_CURRENCY"
146
+ """The symbol's quote currency."""
147
+
148
+
120
149
  class TrendbarPeriod(Enum):
121
150
  """Trendbar/candle period."""
122
151
 
@@ -24,6 +24,7 @@ Example:
24
24
  """
25
25
 
26
26
  from .account import Account, AccountSummary
27
+ from .commission import Commission
27
28
  from .deal import CloseDetail, Deal
28
29
  from .market_data import TickData, Trendbar
29
30
  from .order import Order
@@ -40,6 +41,7 @@ __all__ = [
40
41
  "AmendPositionRequest",
41
42
  "CloseDetail",
42
43
  "ClosePositionRequest",
44
+ "Commission",
43
45
  "Deal",
44
46
  "Holiday",
45
47
  "NewOrderRequest",
@@ -0,0 +1,148 @@
1
+ """What a broker charges to trade a symbol.
2
+
3
+ The server sends a commission as a number and a type, and the type decides what
4
+ the number means. The same rate of ``35`` is 35 USD per million USD of volume for
5
+ one symbol and 35 USD per lot for another. A rate read without its type is
6
+ meaningless, so both travel together here.
7
+
8
+ The server sends each amount twice: once in a deprecated field, and once in a
9
+ scaled ``precise`` field that carries more decimal places. The precise field wins
10
+ where the broker sets it. Brokers that still publish only the deprecated fields
11
+ keep working.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from decimal import Decimal
17
+ from typing import TYPE_CHECKING
18
+
19
+ from .._internal.proto import ProtoOACommissionType, ProtoOAMinCommissionType
20
+ from ..enums import CommissionType, MinCommissionType
21
+ from ._base import FrozenModel
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from .._internal.proto import ProtoOASymbol
26
+
27
+
28
+ _COMMISSION_TYPE_MAP: dict[int, CommissionType] = {
29
+ ProtoOACommissionType.USD_PER_MILLION_USD: CommissionType.USD_PER_MILLION_USD,
30
+ ProtoOACommissionType.USD_PER_LOT: CommissionType.USD_PER_LOT,
31
+ ProtoOACommissionType.PERCENTAGE_OF_VALUE: CommissionType.PERCENTAGE_OF_VALUE,
32
+ ProtoOACommissionType.QUOTE_CCY_PER_LOT: CommissionType.QUOTE_CCY_PER_LOT,
33
+ }
34
+
35
+ _MIN_COMMISSION_TYPE_MAP: dict[int, MinCommissionType] = {
36
+ ProtoOAMinCommissionType.CURRENCY: MinCommissionType.CURRENCY,
37
+ ProtoOAMinCommissionType.QUOTE_CURRENCY: MinCommissionType.QUOTE_CURRENCY,
38
+ }
39
+
40
+ # Every amount is an integer scaled by a fixed power of ten. A money amount is
41
+ # scaled by 10^8 in the precise fields and by 10^2 in the deprecated ones, which is
42
+ # why the two differ by 10^6 on the wire. A percentage rate keeps one scale of 10^5
43
+ # in both fields.
44
+ _PRECISE_SCALE = Decimal(10) ** 8
45
+ _DEPRECATED_SCALE = Decimal(10) ** 2
46
+ _PERCENTAGE_SCALE = Decimal(10) ** 5
47
+
48
+ # The wire format leaves an unset field at 0, and no commission enum has a member
49
+ # there, so 0 means "the broker said nothing" rather than a value.
50
+ _UNSET = 0
51
+
52
+ # What the server applies when the broker omits the field.
53
+ _DEFAULT_MIN_COMMISSION_ASSET = "USD"
54
+
55
+
56
+ class Commission(FrozenModel):
57
+ """The commission a broker charges to trade one symbol.
58
+
59
+ A commission is charged per side, so a round turn costs twice ``rate`` unless
60
+ the broker states otherwise.
61
+
62
+ Attributes:
63
+ type: What ``rate`` counts. See CommissionType.
64
+ rate: The commission rate, in the unit its type names. A
65
+ PERCENTAGE_OF_VALUE rate is a percent, so ``Decimal("0.005")`` means
66
+ 0.005% of the traded value.
67
+ minimum: The smallest commission the broker charges for one trade, in the
68
+ currency its type names. 0 means the broker sets no minimum.
69
+ minimum_type: The currency ``minimum`` is expressed in. See
70
+ MinCommissionType.
71
+ minimum_asset: The currency ``minimum`` is expressed in when
72
+ ``minimum_type`` is CURRENCY.
73
+
74
+ Example:
75
+ ```python
76
+ from ctrader_api_client import CommissionType
77
+
78
+ symbol = await client.symbols.get_by_id(account_id, 270)
79
+ commission = symbol.commission
80
+
81
+ if commission.type is CommissionType.USD_PER_MILLION_USD:
82
+ charge = commission.rate * notional_usd / 1_000_000
83
+ print(f"{max(charge, commission.minimum)} USD per side")
84
+ ```
85
+ """
86
+
87
+ type: CommissionType = CommissionType.USD_PER_MILLION_USD
88
+ rate: Decimal = Decimal(0)
89
+ minimum: Decimal = Decimal(0)
90
+ minimum_type: MinCommissionType = MinCommissionType.CURRENCY
91
+ minimum_asset: str = _DEFAULT_MIN_COMMISSION_ASSET
92
+
93
+ @classmethod
94
+ def from_proto(cls, proto: ProtoOASymbol) -> Commission:
95
+ """Create a Commission from a symbol proto message.
96
+
97
+ Args:
98
+ proto: The proto message to read the commission fields from.
99
+
100
+ Returns:
101
+ A new Commission instance.
102
+ """
103
+ commission_type = _COMMISSION_TYPE_MAP.get(proto.commission_type, CommissionType.USD_PER_MILLION_USD)
104
+ return cls(
105
+ type=commission_type,
106
+ rate=_rate(proto, commission_type),
107
+ minimum=_minimum(proto),
108
+ minimum_type=_MIN_COMMISSION_TYPE_MAP.get(proto.min_commission_type, MinCommissionType.CURRENCY),
109
+ minimum_asset=proto.min_commission_asset or _DEFAULT_MIN_COMMISSION_ASSET,
110
+ )
111
+
112
+
113
+ def _rate(proto: ProtoOASymbol, commission_type: CommissionType) -> Decimal:
114
+ """Descale the commission rate, preferring the precise field over the deprecated one.
115
+
116
+ A percentage rate is scaled by 10^5 in both fields. Every other rate is money,
117
+ so it is scaled by 10^8 in the precise field and by 10^2 in the deprecated one.
118
+
119
+ Args:
120
+ proto: The proto message to read the rate from.
121
+ commission_type: The type the rate is expressed in.
122
+
123
+ Returns:
124
+ The rate in the unit its type names.
125
+ """
126
+ if commission_type is CommissionType.PERCENTAGE_OF_VALUE:
127
+ scaled = proto.precise_trading_commission_rate or proto.commission
128
+ return Decimal(scaled) / _PERCENTAGE_SCALE
129
+ if proto.precise_trading_commission_rate != _UNSET:
130
+ return Decimal(proto.precise_trading_commission_rate) / _PRECISE_SCALE
131
+ return Decimal(proto.commission) / _DEPRECATED_SCALE
132
+
133
+
134
+ def _minimum(proto: ProtoOASymbol) -> Decimal:
135
+ """Descale the minimum commission, preferring the precise field over the deprecated one.
136
+
137
+ A minimum is always money, so it carries the money scales: 10^8 in the precise
138
+ field and 10^2 in the deprecated one.
139
+
140
+ Args:
141
+ proto: The proto message to read the minimum from.
142
+
143
+ Returns:
144
+ The minimum commission per trade.
145
+ """
146
+ if proto.precise_min_commission != _UNSET:
147
+ return Decimal(proto.precise_min_commission) / _PRECISE_SCALE
148
+ return Decimal(proto.min_commission) / _DEPRECATED_SCALE
@@ -8,6 +8,7 @@ from pydantic import Field
8
8
  from .._internal.proto import ProtoOATradingMode
9
9
  from ..enums import TradingMode
10
10
  from ._base import FrozenModel
11
+ from .commission import Commission
11
12
  from .trading_hours import TradingHours
12
13
 
13
14
 
@@ -78,14 +79,15 @@ class Symbol(FrozenModel):
78
79
  symbol_id: The unique symbol identifier.
79
80
  digits: Price decimal places.
80
81
  pip_position: Position of pip in price (e.g., 4 means 0.0001).
81
- lot_size: Contract size in base units (e.g., 100000 for forex).
82
+ lot_size: Contract size in hundredths of a base unit. A forex lot of
83
+ 100000 units reads as 10000000, and a 100 ounce gold lot as 10000.
82
84
  min_volume: Minimum order volume in cents.
83
85
  max_volume: Maximum order volume in cents.
84
86
  step_volume: Volume step in cents.
85
87
  trading_mode: Current trading mode.
86
88
  swap_long: Swap rate for long positions.
87
89
  swap_short: Swap rate for short positions.
88
- commission: Commission rate.
90
+ commission: What the broker charges to trade the symbol.
89
91
  max_exposure: Maximum allowed exposure.
90
92
  leverage_id: ID of dynamic leverage profile, if any.
91
93
  enable_short_selling: Whether short selling is allowed.
@@ -108,9 +110,9 @@ class Symbol(FrozenModel):
108
110
  trading_mode: TradingMode
109
111
  swap_long: Decimal
110
112
  swap_short: Decimal
113
+ commission: Commission = Field(default_factory=Commission)
111
114
 
112
115
  # Optional fields
113
- commission: int = 0
114
116
  max_exposure: int | None = None
115
117
  leverage_id: int | None = None
116
118
  enable_short_selling: bool = True
@@ -176,7 +178,7 @@ class Symbol(FrozenModel):
176
178
  trading_mode=_TRADING_MODE_MAP.get(proto.trading_mode, TradingMode.ENABLED),
177
179
  swap_long=Decimal(str(proto.swap_long)),
178
180
  swap_short=Decimal(str(proto.swap_short)),
179
- commission=proto.commission if proto.commission else 0,
181
+ commission=Commission.from_proto(proto),
180
182
  max_exposure=proto.max_exposure if proto.max_exposure else None,
181
183
  leverage_id=proto.leverage_id if proto.leverage_id else None,
182
184
  enable_short_selling=proto.enable_short_selling,
@@ -0,0 +1,187 @@
1
+ """The translation of the commission a broker charges to trade a symbol."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from decimal import Decimal
6
+
7
+ import pytest
8
+
9
+ from ctrader_api_client._internal.proto import (
10
+ ProtoOACommissionType,
11
+ ProtoOAMinCommissionType,
12
+ ProtoOASymbol,
13
+ )
14
+ from ctrader_api_client.enums import CommissionType, MinCommissionType
15
+ from ctrader_api_client.models import Symbol
16
+
17
+
18
+ @pytest.mark.parametrize(
19
+ ("proto_type", "expected"),
20
+ [
21
+ (ProtoOACommissionType.USD_PER_MILLION_USD, CommissionType.USD_PER_MILLION_USD),
22
+ (ProtoOACommissionType.USD_PER_LOT, CommissionType.USD_PER_LOT),
23
+ (ProtoOACommissionType.PERCENTAGE_OF_VALUE, CommissionType.PERCENTAGE_OF_VALUE),
24
+ (ProtoOACommissionType.QUOTE_CCY_PER_LOT, CommissionType.QUOTE_CCY_PER_LOT),
25
+ ],
26
+ )
27
+ def test_the_commission_type_is_translated(proto_type: ProtoOACommissionType, expected: CommissionType) -> None:
28
+ symbol = Symbol.from_proto(ProtoOASymbol(symbol_id=1, commission_type=proto_type))
29
+
30
+ assert symbol.commission.type == expected
31
+
32
+
33
+ def test_a_commission_type_the_broker_omits_falls_back_to_the_server_default() -> None:
34
+ """The wire format leaves an unset enum at 0, which no commission type uses."""
35
+ symbol = Symbol.from_proto(ProtoOASymbol(symbol_id=1))
36
+
37
+ assert symbol.commission.type == CommissionType.USD_PER_MILLION_USD
38
+ assert symbol.commission.minimum_type == MinCommissionType.CURRENCY
39
+ assert symbol.commission.minimum_asset == "USD"
40
+
41
+
42
+ def test_a_symbol_without_a_commission_reports_no_charge() -> None:
43
+ symbol = Symbol.from_proto(ProtoOASymbol(symbol_id=1))
44
+
45
+ assert symbol.commission.rate == Decimal(0)
46
+ assert symbol.commission.minimum == Decimal(0)
47
+
48
+
49
+ def test_a_rate_per_million_is_descaled() -> None:
50
+ symbol = Symbol.from_proto(
51
+ ProtoOASymbol(
52
+ symbol_id=270,
53
+ commission_type=ProtoOACommissionType.USD_PER_MILLION_USD,
54
+ precise_trading_commission_rate=3_500_000_000,
55
+ )
56
+ )
57
+
58
+ assert symbol.commission.rate == Decimal(35)
59
+
60
+
61
+ def test_a_rate_per_lot_keeps_its_fractional_part() -> None:
62
+ """The precise field exists to carry the decimals the deprecated one cannot."""
63
+ symbol = Symbol.from_proto(
64
+ ProtoOASymbol(
65
+ symbol_id=270,
66
+ commission_type=ProtoOACommissionType.USD_PER_LOT,
67
+ precise_trading_commission_rate=350_000_000,
68
+ )
69
+ )
70
+
71
+ assert symbol.commission.rate == Decimal("3.5")
72
+
73
+
74
+ def test_a_percentage_rate_is_reported_as_a_percent() -> None:
75
+ """A percentage rate is scaled by 10^5, not by the 10^8 the other types use."""
76
+ symbol = Symbol.from_proto(
77
+ ProtoOASymbol(
78
+ symbol_id=270,
79
+ commission_type=ProtoOACommissionType.PERCENTAGE_OF_VALUE,
80
+ precise_trading_commission_rate=500,
81
+ )
82
+ )
83
+
84
+ assert symbol.commission.rate == Decimal("0.005")
85
+
86
+
87
+ def test_a_broker_that_sends_only_the_deprecated_rate_is_read() -> None:
88
+ """A deprecated money rate counts hundredths, so 250 is 2.50 USD per lot."""
89
+ symbol = Symbol.from_proto(
90
+ ProtoOASymbol(
91
+ symbol_id=270,
92
+ commission_type=ProtoOACommissionType.USD_PER_LOT,
93
+ commission=250,
94
+ )
95
+ )
96
+
97
+ assert symbol.commission.rate == Decimal("2.5")
98
+
99
+
100
+ def test_a_deprecated_percentage_rate_carries_the_percentage_scale() -> None:
101
+ symbol = Symbol.from_proto(
102
+ ProtoOASymbol(
103
+ symbol_id=270,
104
+ commission_type=ProtoOACommissionType.PERCENTAGE_OF_VALUE,
105
+ commission=500,
106
+ )
107
+ )
108
+
109
+ assert symbol.commission.rate == Decimal("0.005")
110
+
111
+
112
+ def test_the_precise_rate_wins_over_the_deprecated_one() -> None:
113
+ """Brokers send both, and only the precise field carries the smaller decimals."""
114
+ symbol = Symbol.from_proto(
115
+ ProtoOASymbol(
116
+ symbol_id=270,
117
+ commission_type=ProtoOACommissionType.USD_PER_LOT,
118
+ commission=355,
119
+ precise_trading_commission_rate=355_500_000,
120
+ )
121
+ )
122
+
123
+ assert symbol.commission.rate == Decimal("3.555")
124
+
125
+
126
+ def test_the_two_rate_fields_agree_on_what_a_live_broker_sends() -> None:
127
+ """A live broker publishes 2.50 USD per lot as 250 and as 250000000.
128
+
129
+ The two scales differ by 10^6, and both readings must give the same money.
130
+ """
131
+ symbol = Symbol.from_proto(
132
+ ProtoOASymbol(
133
+ symbol_id=270,
134
+ commission_type=ProtoOACommissionType.USD_PER_LOT,
135
+ commission=250,
136
+ precise_trading_commission_rate=250_000_000,
137
+ )
138
+ )
139
+
140
+ assert symbol.commission.rate == Decimal("2.5")
141
+
142
+
143
+ def test_a_live_percentage_rate_reads_as_a_fraction_of_a_percent() -> None:
144
+ """A live broker publishes 0.0007% on gold as 70 in both rate fields."""
145
+ symbol = Symbol.from_proto(
146
+ ProtoOASymbol(
147
+ symbol_id=41,
148
+ commission_type=ProtoOACommissionType.PERCENTAGE_OF_VALUE,
149
+ commission=70,
150
+ precise_trading_commission_rate=70,
151
+ )
152
+ )
153
+
154
+ assert symbol.commission.rate == Decimal("0.0007")
155
+
156
+
157
+ def test_the_minimum_commission_is_descaled() -> None:
158
+ symbol = Symbol.from_proto(ProtoOASymbol(symbol_id=270, precise_min_commission=250_000_000))
159
+
160
+ assert symbol.commission.minimum == Decimal("2.5")
161
+
162
+
163
+ def test_a_broker_that_sends_only_the_deprecated_minimum_is_read() -> None:
164
+ """A minimum is money, so the deprecated field counts hundredths as the rate does."""
165
+ symbol = Symbol.from_proto(ProtoOASymbol(symbol_id=270, min_commission=250))
166
+
167
+ assert symbol.commission.minimum == Decimal("2.5")
168
+
169
+
170
+ def test_the_precise_minimum_wins_over_the_deprecated_one() -> None:
171
+ symbol = Symbol.from_proto(ProtoOASymbol(symbol_id=270, min_commission=200, precise_min_commission=250_000_000))
172
+
173
+ assert symbol.commission.minimum == Decimal("2.5")
174
+
175
+
176
+ def test_the_minimum_commission_keeps_the_currency_it_is_charged_in() -> None:
177
+ symbol = Symbol.from_proto(
178
+ ProtoOASymbol(
179
+ symbol_id=270,
180
+ precise_min_commission=250_000_000,
181
+ min_commission_type=ProtoOAMinCommissionType.QUOTE_CURRENCY,
182
+ min_commission_asset="EUR",
183
+ )
184
+ )
185
+
186
+ assert symbol.commission.minimum_type == MinCommissionType.QUOTE_CURRENCY
187
+ assert symbol.commission.minimum_asset == "EUR"
@@ -270,7 +270,7 @@ wheels = [
270
270
 
271
271
  [[package]]
272
272
  name = "ctrader-api-client"
273
- version = "0.8.1"
273
+ version = "0.9.0"
274
274
  source = { editable = "." }
275
275
  dependencies = [
276
276
  { name = "anyio" },