ctrader-api-client 0.8.0__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 (128) hide show
  1. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/PKG-INFO +18 -2
  2. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/README.md +15 -0
  3. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/docs/api/enums.md +10 -0
  4. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/docs/api/models.md +80 -0
  5. ctrader_api_client-0.9.0/docs/api/symbols.md +172 -0
  6. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/pyproject.toml +4 -1
  7. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/__init__.py +16 -0
  8. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/enums.py +29 -0
  9. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/exceptions.py +22 -0
  10. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/__init__.py +7 -0
  11. ctrader_api_client-0.9.0/src/ctrader_api_client/models/commission.py +148 -0
  12. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/symbol.py +13 -4
  13. ctrader_api_client-0.9.0/src/ctrader_api_client/models/trading_hours.py +506 -0
  14. ctrader_api_client-0.9.0/tests/unit/test_commission_conversion.py +187 -0
  15. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/unit/test_symbol_conversion.py +19 -1
  16. ctrader_api_client-0.9.0/tests/unit/test_trading_hours.py +387 -0
  17. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/uv.lock +12 -1
  18. ctrader_api_client-0.8.0/docs/api/symbols.md +0 -85
  19. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/.claude/settings.local.json +0 -0
  20. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/.github/workflows/docs.yml +0 -0
  21. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/.gitignore +0 -0
  22. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/.pre-commit-config.yaml +0 -0
  23. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/.python-version +0 -0
  24. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/Justfile +0 -0
  25. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/LICENSE +0 -0
  26. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/docs/api/accounts.md +0 -0
  27. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/docs/api/client.md +0 -0
  28. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/docs/api/events.md +0 -0
  29. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/docs/api/exceptions.md +0 -0
  30. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/docs/api/market-data.md +0 -0
  31. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/docs/api/trading.md +0 -0
  32. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/docs/getting-started.md +0 -0
  33. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/docs/index.md +0 -0
  34. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/mkdocs.yml +0 -0
  35. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/protos/SOURCE +0 -0
  36. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/protos/VERSION +0 -0
  37. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/protos/update.sh +0 -0
  38. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
  39. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
  40. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/protos/vendor/OpenApiMessages.proto +0 -0
  41. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/protos/vendor/OpenApiModelMessages.proto +0 -0
  42. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/scripts/fix_proto_imports.py +0 -0
  43. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/__init__.py +0 -0
  44. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/clock.py +0 -0
  45. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/conversions.py +0 -0
  46. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/messages.py +0 -0
  47. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
  48. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
  49. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
  50. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
  51. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
  52. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
  53. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/__init__.py +0 -0
  54. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/_base.py +0 -0
  55. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/accounts.py +0 -0
  56. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/market_data.py +0 -0
  57. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/symbols.py +0 -0
  58. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/api/trading.py +0 -0
  59. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/__init__.py +0 -0
  60. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/_recovery.py +0 -0
  61. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/_refresh.py +0 -0
  62. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/_session.py +0 -0
  63. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/credentials.py +0 -0
  64. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/manager.py +0 -0
  65. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/policy.py +0 -0
  66. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/auth/store.py +0 -0
  67. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/client.py +0 -0
  68. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/composition.py +0 -0
  69. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/config.py +0 -0
  70. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/__init__.py +0 -0
  71. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/heartbeat.py +0 -0
  72. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/listener.py +0 -0
  73. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/protocol.py +0 -0
  74. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/supervisor.py +0 -0
  75. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/connection/transport.py +0 -0
  76. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/__init__.py +0 -0
  77. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/_execution.py +0 -0
  78. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/emitter.py +0 -0
  79. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/publisher.py +0 -0
  80. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/router.py +0 -0
  81. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/events/types.py +0 -0
  82. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/_base.py +0 -0
  83. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/account.py +0 -0
  84. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/deal.py +0 -0
  85. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/market_data.py +0 -0
  86. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/order.py +0 -0
  87. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/position.py +0 -0
  88. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/models/requests.py +0 -0
  89. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/src/ctrader_api_client/py.typed +0 -0
  90. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/__init__.py +0 -0
  91. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/__init__.py +0 -0
  92. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/api/__init__.py +0 -0
  93. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/api/conftest.py +0 -0
  94. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/api/test_market_data.py +0 -0
  95. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/api/test_reference_data.py +0 -0
  96. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/api/test_subscription_restore.py +0 -0
  97. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/api/test_trading.py +0 -0
  98. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/auth/__init__.py +0 -0
  99. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/auth/conftest.py +0 -0
  100. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/auth/test_authentication.py +0 -0
  101. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/auth/test_token_lifecycle.py +0 -0
  102. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/connection/__init__.py +0 -0
  103. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/connection/conftest.py +0 -0
  104. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/connection/test_event_dispatch.py +0 -0
  105. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/connection/test_framing.py +0 -0
  106. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/connection/test_heartbeat.py +0 -0
  107. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/connection/test_reconnection.py +0 -0
  108. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/connection/test_request_response.py +0 -0
  109. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/events/__init__.py +0 -0
  110. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/events/conftest.py +0 -0
  111. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/events/test_routing.py +0 -0
  112. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/events/test_subscriptions.py +0 -0
  113. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/behavior/test_client.py +0 -0
  114. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/conftest.py +0 -0
  115. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/harness/__init__.py +0 -0
  116. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/harness/clock.py +0 -0
  117. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/harness/factories.py +0 -0
  118. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/harness/recorder.py +0 -0
  119. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/harness/server.py +0 -0
  120. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/harness/signals.py +0 -0
  121. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/harness/stub_protocol.py +0 -0
  122. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/harness/wire.py +0 -0
  123. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/unit/__init__.py +0 -0
  124. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/unit/test_api_errors.py +0 -0
  125. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/unit/test_market_data_conversion.py +0 -0
  126. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/unit/test_request_conversion.py +0 -0
  127. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/unit/test_trading_records.py +0 -0
  128. {ctrader_api_client-0.8.0 → ctrader_api_client-0.9.0}/tests/unit/test_wire_format.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: ctrader-api-client
3
- Version: 0.8.0
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
@@ -15,6 +15,7 @@ Requires-Dist: anyio>=4.13.0
15
15
  Requires-Dist: betterproto[compiler]>=1.2.5
16
16
  Requires-Dist: pydantic>=2.12.5
17
17
  Requires-Dist: tenacity>=9.1.4
18
+ Requires-Dist: tzdata>=2025.2
18
19
  Description-Content-Type: text/markdown
19
20
 
20
21
  # cTrader API Client
@@ -301,6 +302,21 @@ results = await client.symbols.search(account_id, "EUR")
301
302
 
302
303
  # Get specific symbol
303
304
  symbol = await client.symbols.get_by_id(account_id, symbol_id)
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
+
310
+ # Trading hours arrive with the symbol, so this costs no extra request
311
+ hours = symbol.trading_hours
312
+ if hours.is_open():
313
+ print(f"Open, closes at {hours.next_close()}")
314
+ else:
315
+ print(f"Closed, opens at {hours.next_open()}")
316
+
317
+ # Sessions for a given day, with broker holidays already applied
318
+ for session in hours.sessions_on(date(2026, 6, 19)):
319
+ print(session.start, "->", session.end)
304
320
  ```
305
321
 
306
322
  ### Account Information
@@ -282,6 +282,21 @@ results = await client.symbols.search(account_id, "EUR")
282
282
 
283
283
  # Get specific symbol
284
284
  symbol = await client.symbols.get_by_id(account_id, symbol_id)
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
+
290
+ # Trading hours arrive with the symbol, so this costs no extra request
291
+ hours = symbol.trading_hours
292
+ if hours.is_open():
293
+ print(f"Open, closes at {hours.next_close()}")
294
+ else:
295
+ print(f"Closed, opens at {hours.next_open()}")
296
+
297
+ # Sessions for a given day, with broker holidays already applied
298
+ for session in hours.sessions_on(date(2026, 6, 19)):
299
+ print(session.start, "->", session.end)
285
300
  ```
286
301
 
287
302
  ### Account Information
@@ -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,86 @@ 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
+
193
+ ::: ctrader_api_client.models.TradingHours
194
+ options:
195
+ show_source: false
196
+
197
+
198
+ ::: ctrader_api_client.models.TradingSession
199
+ options:
200
+ show_source: false
201
+
202
+
203
+ ::: ctrader_api_client.models.ScheduleInterval
204
+ options:
205
+ show_source: false
206
+
207
+
208
+ ::: ctrader_api_client.models.Holiday
209
+ options:
210
+ show_source: false
211
+
212
+
133
213
  ::: ctrader_api_client.models.Account
134
214
  options:
135
215
  show_source: false
@@ -0,0 +1,172 @@
1
+ # Symbols API
2
+
3
+ Symbol information lookup and search operations.
4
+
5
+ Access via `client.symbols`.
6
+
7
+ ## SymbolsAPI
8
+
9
+ ::: ctrader_api_client.api.SymbolsAPI
10
+ options:
11
+ show_source: false
12
+ members:
13
+ - list_all
14
+ - get_by_ids
15
+ - get_by_id
16
+ - search
17
+
18
+ ## Usage Examples
19
+
20
+ ### List All Symbols
21
+
22
+ ```python
23
+ # Get lightweight info for all symbols
24
+ symbols = await client.symbols.list_all(account_id)
25
+
26
+ for sym in symbols:
27
+ print(f"{sym.symbol_id}: {sym.name}")
28
+ ```
29
+
30
+ ### Get Symbol by ID
31
+
32
+ ```python
33
+ # Get full symbol details
34
+ symbol = await client.symbols.get_by_id(account_id, 270)
35
+
36
+ print(f"Digits: {symbol.digits}")
37
+ print(f"Lot size: {symbol.lot_size}")
38
+ print(f"Min volume: {symbol.min_volume}")
39
+ print(f"Max volume: {symbol.max_volume}")
40
+ ```
41
+
42
+ ### Get Multiple Symbols
43
+
44
+ ```python
45
+ symbols = await client.symbols.get_by_ids(account_id, [270, 271, 272])
46
+
47
+ for sym in symbols:
48
+ print(f"{sym.name}: {sym.digits} digits, lot_size={sym.lot_size}")
49
+ ```
50
+
51
+ ### Search Symbols
52
+
53
+ ```python
54
+ # Find all EUR pairs
55
+ eur_pairs = await client.symbols.search(account_id, "EUR")
56
+
57
+ for sym in eur_pairs:
58
+ print(sym.name)
59
+ ```
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
+
90
+ ## Trading Hours
91
+
92
+ Every `Symbol` arrives with its trading calendar attached, so asking when a symbol is open
93
+ costs no extra request:
94
+
95
+ ```python
96
+ from datetime import date
97
+
98
+ symbol = await client.symbols.get_by_id(account_id, 270)
99
+ hours = symbol.trading_hours
100
+
101
+ if hours.is_open():
102
+ print("open now, closes at", hours.next_close())
103
+ else:
104
+ print("closed, opens at", hours.next_open())
105
+
106
+ for session in hours.sessions_on(date(2026, 6, 19)):
107
+ print(session.start, "->", session.end, f"({session.duration})")
108
+ ```
109
+
110
+ Sessions are reported in the symbol's own `schedule_timezone` and are never merged, so a
111
+ schedule with a nightly maintenance break reports one session either side of it.
112
+
113
+ ### Holidays
114
+
115
+ Holidays are closed windows that cut into the weekly schedule rather than replacing a whole
116
+ day. A broker publishing an "early close" sends a window covering the rest of the day, which
117
+ brings the close forward:
118
+
119
+ ```python
120
+ # Regular Friday: 01:05 - 23:50. With a 20:00 early close holiday:
121
+ hours.sessions_on(date(2026, 6, 19)) # -> one session, 01:05 - 20:00
122
+
123
+ hours.holidays_on(date(2026, 6, 19)) # -> the holiday responsible
124
+ ```
125
+
126
+ A holiday carries **its own** timezone, which is not always the symbol's. A closure defined in
127
+ a timezone an hour behind the symbol's straddles two of the symbol's calendar days, and both
128
+ are shortened accordingly.
129
+
130
+ A holiday whose window is empty (`start_second >= end_second`, which brokers send as `0`/`0`)
131
+ closes the entire day.
132
+
133
+ ### Trading mode is separate
134
+
135
+ `is_open()` answers the calendar question only. A symbol can be closed to trading by its
136
+ `trading_mode` while the schedule says it is open, so check both:
137
+
138
+ ```python
139
+ tradable = hours.is_open() and symbol.trading_mode == TradingMode.ENABLED
140
+ ```
141
+
142
+ ### Staleness
143
+
144
+ Schedules arrive only with a symbol fetch, so a long-lived `Symbol` can drift. Subscribe to
145
+ `SymbolChangedEvent` and re-fetch when the server reports a change. The API exposes no
146
+ historical schedules — what you receive is the current configuration.
147
+
148
+ ## Volume Conversion
149
+
150
+ The cTrader API uses volume in "cents" (smallest volume units). The relationship between lots and volume depends on the symbol's `lot_size`:
151
+
152
+ ```python
153
+ symbol = await client.symbols.get_by_id(account_id, 270)
154
+
155
+ # Lots to volume (for placing orders)
156
+ volume = symbol.lots_to_volume(1.0) # e.g., 10000000 for standard forex
157
+
158
+ # Volume to lots (for display)
159
+ lots = symbol.volume_to_lots(10000000) # e.g., 1.0 for standard forex
160
+ ```
161
+
162
+ **Note:** Different instruments have different lot sizes. Always use the symbol's methods for conversion:
163
+
164
+ ```python
165
+ # Standard forex (lot_size=100000)
166
+ forex_symbol = await client.symbols.get_by_id(account_id, 1) # EURUSD (lot_size=10000000)
167
+ forex_volume = forex_symbol.lots_to_volume(0.1) # 1000000
168
+
169
+ # Index CFD might have different lot_size
170
+ index_symbol = await client.symbols.get_by_id(account_id, 270) # US500 (lot_size=100)
171
+ index_volume = index_symbol.lots_to_volume(0.1) # 10
172
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctrader-api-client"
3
- version = "0.8.0"
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 = [
@@ -20,6 +20,9 @@ dependencies = [
20
20
  "betterproto[compiler]>=1.2.5",
21
21
  "pydantic>=2.12.5",
22
22
  "tenacity>=9.1.4",
23
+ # Windows ships no system IANA database, which ZoneInfo needs to resolve the
24
+ # broker-supplied timezones that trading schedules are expressed in.
25
+ "tzdata>=2025.2",
23
26
  ]
24
27
 
25
28
  [build-system]
@@ -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,
@@ -99,6 +101,7 @@ from .exceptions import (
99
101
  TokenExpiredError,
100
102
  TokenRefreshError,
101
103
  UnknownPayloadTypeError,
104
+ UnknownTimezoneError,
102
105
  )
103
106
  from .models import (
104
107
  Account,
@@ -107,14 +110,19 @@ from .models import (
107
110
  AmendPositionRequest,
108
111
  CloseDetail,
109
112
  ClosePositionRequest,
113
+ Commission,
110
114
  Deal,
115
+ Holiday,
111
116
  NewOrderRequest,
112
117
  Order,
113
118
  Position,
114
119
  PositionUnrealizedPnL,
120
+ ScheduleInterval,
115
121
  Symbol,
116
122
  SymbolInfo,
117
123
  TickData,
124
+ TradingHours,
125
+ TradingSession,
118
126
  Trendbar,
119
127
  )
120
128
 
@@ -145,6 +153,8 @@ __all__ = [
145
153
  "ClientDisconnectEvent",
146
154
  "CloseDetail",
147
155
  "ClosePositionRequest",
156
+ "Commission",
157
+ "CommissionType",
148
158
  "Deal",
149
159
  "DealStatus",
150
160
  "DepthEvent",
@@ -154,8 +164,10 @@ __all__ = [
154
164
  "ExecutionEvent",
155
165
  "ExecutionType",
156
166
  "FramingError",
167
+ "Holiday",
157
168
  "MarginCallTriggerEvent",
158
169
  "MarginChangeEvent",
170
+ "MinCommissionType",
159
171
  "NewOrderRequest",
160
172
  "Order",
161
173
  "OrderErrorEvent",
@@ -170,6 +182,7 @@ __all__ = [
170
182
  "ReauthPolicy",
171
183
  "ReconnectedEvent",
172
184
  "RefreshPolicy",
185
+ "ScheduleInterval",
173
186
  "SpotEvent",
174
187
  "StopTriggerMethod",
175
188
  "SubscriptionRestoreFailedEvent",
@@ -184,9 +197,12 @@ __all__ = [
184
197
  "TokenRefreshFailedEvent",
185
198
  "TokenStore",
186
199
  "TraderUpdateEvent",
200
+ "TradingHours",
187
201
  "TradingMode",
202
+ "TradingSession",
188
203
  "TrailingStopChangedEvent",
189
204
  "Trendbar",
190
205
  "TrendbarPeriod",
191
206
  "UnknownPayloadTypeError",
207
+ "UnknownTimezoneError",
192
208
  ]
@@ -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
 
@@ -255,6 +255,28 @@ class APIError(CTraderError):
255
255
  )
256
256
 
257
257
 
258
+ # =============================================================================
259
+ # Data Errors
260
+ # =============================================================================
261
+
262
+
263
+ class UnknownTimezoneError(CTraderError):
264
+ """A timezone named by the server is not in the IANA database.
265
+
266
+ Trading schedules are broker-supplied wall clock time, so a timezone that
267
+ cannot be resolved leaves every session boundary meaningless. This should
268
+ never happen: if it does, the broker has published a symbol whose schedule
269
+ timezone is misconfigured, and the schedule cannot be interpreted at all.
270
+ """
271
+
272
+ def __init__(self, name: str) -> None:
273
+ self.name = name
274
+ super().__init__(
275
+ f"Unknown schedule timezone {name!r}. This should never happen; it means the broker "
276
+ f"published a symbol whose schedule timezone is not a valid IANA name."
277
+ )
278
+
279
+
258
280
  # =============================================================================
259
281
  # Protocol Errors
260
282
  # =============================================================================
@@ -24,12 +24,14 @@ 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
30
31
  from .position import Position, PositionUnrealizedPnL
31
32
  from .requests import AmendOrderRequest, AmendPositionRequest, ClosePositionRequest, NewOrderRequest
32
33
  from .symbol import Symbol, SymbolInfo
34
+ from .trading_hours import Holiday, ScheduleInterval, TradingHours, TradingSession
33
35
 
34
36
 
35
37
  __all__ = [
@@ -39,13 +41,18 @@ __all__ = [
39
41
  "AmendPositionRequest",
40
42
  "CloseDetail",
41
43
  "ClosePositionRequest",
44
+ "Commission",
42
45
  "Deal",
46
+ "Holiday",
43
47
  "NewOrderRequest",
44
48
  "Order",
45
49
  "Position",
46
50
  "PositionUnrealizedPnL",
51
+ "ScheduleInterval",
47
52
  "Symbol",
48
53
  "SymbolInfo",
49
54
  "TickData",
55
+ "TradingHours",
56
+ "TradingSession",
50
57
  "Trendbar",
51
58
  ]
@@ -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