ctrader-api-client 0.1.2__tar.gz → 0.2.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 (95) hide show
  1. ctrader_api_client-0.2.0/.claude/settings.local.json +7 -0
  2. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/Justfile +1 -1
  3. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/PKG-INFO +1 -1
  4. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/accounts.md +1 -1
  5. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/events.md +20 -0
  6. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/market-data.md +33 -7
  7. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/models.md +56 -25
  8. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/symbols.md +16 -22
  9. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/trading.md +60 -6
  10. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/getting-started.md +9 -6
  11. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/index.md +1 -0
  12. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/pyproject.toml +5 -6
  13. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/api/market_data.py +8 -4
  14. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/api/trading.py +80 -3
  15. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/client.py +2 -2
  16. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/events/router.py +4 -2
  17. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/events/types.py +7 -8
  18. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/account.py +14 -24
  19. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/deal.py +26 -91
  20. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/market_data.py +57 -94
  21. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/order.py +0 -63
  22. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/position.py +9 -72
  23. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/requests.py +16 -17
  24. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/symbol.py +5 -30
  25. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/_internal/test_messages.py +2 -2
  26. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/api/test_accounts.py +1 -2
  27. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/api/test_market_data_api.py +1 -1
  28. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/events/test_emitter.py +24 -16
  29. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/events/test_router.py +7 -3
  30. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/events/test_types.py +11 -7
  31. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_account.py +12 -52
  32. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_deal.py +57 -195
  33. ctrader_api_client-0.2.0/tests/unit/models/test_market_data.py +159 -0
  34. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_order.py +1 -130
  35. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_position.py +8 -168
  36. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_requests.py +3 -2
  37. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_symbol.py +39 -86
  38. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/uv.lock +22 -27
  39. ctrader_api_client-0.1.2/tests/unit/models/test_market_data.py +0 -265
  40. ctrader_api_client-0.1.2/tests/unit/test_config.py +0 -149
  41. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/.github/workflows/docs.yml +0 -0
  42. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/.gitignore +0 -0
  43. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/.pre-commit-config.yaml +0 -0
  44. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/.python-version +0 -0
  45. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/LICENSE +0 -0
  46. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/README.md +0 -0
  47. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/client.md +0 -0
  48. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/enums.md +0 -0
  49. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/mkdocs.yml +0 -0
  50. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/SOURCE +0 -0
  51. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/VERSION +0 -0
  52. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/update.sh +0 -0
  53. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/vendor/.gitkeep +0 -0
  54. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
  55. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
  56. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/vendor/OpenApiMessages.proto +0 -0
  57. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/vendor/OpenApiModelMessages.proto +0 -0
  58. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/scripts/fix_proto_imports.py +0 -0
  59. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/__init__.py +0 -0
  60. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/__init__.py +0 -0
  61. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/messages.py +0 -0
  62. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
  63. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
  64. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
  65. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
  66. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
  67. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
  68. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/api/__init__.py +0 -0
  69. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/api/accounts.py +0 -0
  70. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/api/symbols.py +0 -0
  71. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/auth/__init__.py +0 -0
  72. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/auth/credentials.py +0 -0
  73. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/auth/manager.py +0 -0
  74. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/config.py +0 -0
  75. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/connection/__init__.py +0 -0
  76. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/connection/heartbeat.py +0 -0
  77. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/connection/protocol.py +0 -0
  78. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/connection/transport.py +0 -0
  79. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/enums.py +0 -0
  80. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/events/__init__.py +0 -0
  81. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/events/emitter.py +0 -0
  82. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/exceptions.py +0 -0
  83. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/__init__.py +0 -0
  84. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/_base.py +0 -0
  85. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/py.typed +0 -0
  86. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/_internal/test_serialization.py +0 -0
  87. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/api/conftest.py +0 -0
  88. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/api/test_symbols.py +0 -0
  89. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/api/test_trading.py +0 -0
  90. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/auth/test_credentials.py +0 -0
  91. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/auth/test_manager.py +0 -0
  92. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/connection/test_heartbeat.py +0 -0
  93. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/connection/test_protocol.py +0 -0
  94. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/connection/test_transport.py +0 -0
  95. {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/test_client.py +0 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git -C \"/home/tek/Desktop/Algo Trading/Live Trading/cTrader/ctrader-api-client\" log --oneline -10)"
5
+ ]
6
+ }
7
+ }
@@ -26,7 +26,7 @@ fmt directory='':
26
26
 
27
27
  # Run type checking using ty, optionally specifying a directory to check.
28
28
  type-check directory='':
29
- uv run ty check {{directory}}
29
+ uv run zuban check {{directory}}
30
30
 
31
31
  # Run tests using pytest, optionally specifying a directory to test.
32
32
  test directory='':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctrader-api-client
3
- Version: 0.1.2
3
+ Version: 0.2.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
@@ -19,7 +19,7 @@ Access via `client.accounts`.
19
19
  ```python
20
20
  account = await client.accounts.get_trader(account_id)
21
21
 
22
- print(f"Balance: {account.get_balance()}")
22
+ print(f"Balance: {account.balance}")
23
23
  print(f"Leverage: {account.get_leverage()}")
24
24
  print(f"Account type: {account.account_type}")
25
25
  print(f"Broker name: {account.broker_name}")
@@ -41,6 +41,26 @@ Using an unsupported filter raises `ValueError` at registration time.
41
41
  options:
42
42
  show_source: false
43
43
 
44
+ **SpotEvent contains live trendbar data when subscribed:**
45
+
46
+ ```python
47
+ from ctrader_api_client.enums import TrendbarPeriod
48
+
49
+ # Subscribe to both spot prices and M1 trendbars
50
+ await client.market_data.subscribe_spots(account_id, [270])
51
+ await client.market_data.subscribe_trendbars(account_id, 270, TrendbarPeriod.M1)
52
+
53
+ @client.on(SpotEvent, symbol_id=270)
54
+ async def on_spot(event: SpotEvent):
55
+ # Prices are floats
56
+ print(f"Bid: {event.bid}, Ask: {event.ask}")
57
+
58
+ # Trendbar is included when subscribed
59
+ if event.trendbar:
60
+ bar = event.trendbar
61
+ print(f"Candle: O={bar.open} H={bar.high} L={bar.low} C={bar.close}")
62
+ ```
63
+
44
64
  ::: ctrader_api_client.events.DepthEvent
45
65
  options:
46
66
  show_source: false
@@ -30,12 +30,32 @@ from ctrader_api_client.events import SpotEvent
30
30
  await client.market_data.subscribe_spots(account_id, [270, 271])
31
31
 
32
32
 
33
- # Handle price updates
33
+ # Handle price updates - bid/ask are floats
34
34
  @client.on(SpotEvent, symbol_id=270)
35
35
  async def on_price(event: SpotEvent):
36
36
  print(f"Bid: {event.bid}, Ask: {event.ask}")
37
37
  ```
38
38
 
39
+ ### Subscribe to Live Trendbars
40
+
41
+ ```python
42
+ from ctrader_api_client.events import SpotEvent
43
+ from ctrader_api_client.enums import TrendbarPeriod
44
+
45
+ # Subscribe to M1 trendbars
46
+ await client.market_data.subscribe_trendbars(account_id, symbol_id=270, period=TrendbarPeriod.M1)
47
+
48
+ # Trendbar data is delivered inside SpotEvent
49
+ @client.on(SpotEvent, symbol_id=270)
50
+ async def on_spot(event: SpotEvent):
51
+ print(f"Price: {event.bid}/{event.ask}")
52
+
53
+ # Check if this event contains trendbar data
54
+ if event.trendbar:
55
+ bar = event.trendbar
56
+ print(f"Bar: O={bar.open} H={bar.high} L={bar.low} C={bar.close} V={bar.volume}")
57
+ ```
58
+
39
59
  ### Subscribe to Depth of Market
40
60
 
41
61
  ```python
@@ -55,32 +75,36 @@ async def on_depth(event: DepthEvent):
55
75
  ### Get Historical Trendbars
56
76
 
57
77
  ```python
58
- from datetime import datetime, timedelta
78
+ from datetime import datetime, timedelta, UTC
59
79
  from ctrader_api_client.enums import TrendbarPeriod
60
80
 
61
81
  trendbars = await client.market_data.get_trendbars(
62
82
  account_id,
63
83
  symbol_id=270,
64
84
  period=TrendbarPeriod.H1,
65
- from_timestamp=datetime.now() - timedelta(days=7),
66
- to_timestamp=datetime.now(),
85
+ from_timestamp=datetime.now(UTC) - timedelta(days=7),
86
+ to_timestamp=datetime.now(UTC),
67
87
  )
68
88
 
89
+ # OHLC values are already floats (converted from raw integers)
69
90
  for bar in trendbars:
70
- print(f"O:{bar.open} H:{bar.high} L:{bar.low} C:{bar.close} V:{bar.volume}")
91
+ print(f"{bar.timestamp}: O={bar.open} H={bar.high} L={bar.low} C={bar.close} V={bar.volume}")
71
92
  ```
72
93
 
73
94
  ### Get Tick Data
74
95
 
75
96
  ```python
97
+ from datetime import datetime, timedelta, UTC
98
+
76
99
  ticks = await client.market_data.get_tick_data(
77
100
  account_id,
78
101
  symbol_id=270,
79
- from_timestamp=datetime.now() - timedelta(hours=1),
80
- to_timestamp=datetime.now(),
102
+ from_timestamp=datetime.now(UTC) - timedelta(hours=1),
103
+ to_timestamp=datetime.now(UTC),
81
104
  quote_type="BID", # or "ASK"
82
105
  )
83
106
 
107
+ # Price is already a float
84
108
  for tick in ticks:
85
109
  print(f"{tick.timestamp}: {tick.price}")
86
110
  ```
@@ -109,9 +133,11 @@ It is recommended to use `ReadyEvent` to keep all subscriptions centralized in o
109
133
 
110
134
  ```python
111
135
  from ctrader_api_client.events import ReadyEvent
136
+ from ctrader_api_client.enums import TrendbarPeriod
112
137
 
113
138
  @client.on(ReadyEvent)
114
139
  async def on_ready(event: ReadyEvent):
115
140
  # This runs on initial auth AND after reconnection
116
141
  await client.market_data.subscribe_spots(event.account_id, [270, 271])
142
+ await client.market_data.subscribe_trendbars(event.account_id, 270, TrendbarPeriod.M1)
117
143
  ```
@@ -16,12 +16,15 @@ Request for placing a new order.
16
16
  from ctrader_api_client.models import NewOrderRequest
17
17
  from ctrader_api_client.enums import OrderType, OrderSide, TimeInForce
18
18
 
19
+ # Get symbol for volume conversion
20
+ symbol = await client.symbols.get_by_id(account_id, 270)
21
+
19
22
  # Market order
20
23
  market_order = NewOrderRequest(
21
24
  symbol_id=270,
22
25
  order_type=OrderType.MARKET,
23
26
  side=OrderSide.BUY,
24
- volume=100, # 0.01 lots
27
+ volume=symbol.lots_to_volume(0.01), # 0.01 lots
25
28
  )
26
29
 
27
30
  # Limit order with SL/TP
@@ -29,19 +32,29 @@ limit_order = NewOrderRequest(
29
32
  symbol_id=270,
30
33
  order_type=OrderType.LIMIT,
31
34
  side=OrderSide.BUY,
32
- volume=100,
35
+ volume=symbol.lots_to_volume(0.1),
33
36
  limit_price=5000.0,
34
37
  stop_loss=4950.0,
35
38
  take_profit=5100.0,
36
39
  time_in_force=TimeInForce.GOOD_TILL_CANCEL,
37
40
  )
38
41
 
42
+ # Order with relative SL/TP (distance from entry price)
43
+ relative_order = NewOrderRequest(
44
+ symbol_id=270,
45
+ order_type=OrderType.MARKET,
46
+ side=OrderSide.BUY,
47
+ volume=symbol.lots_to_volume(0.1),
48
+ relative_stop_loss=50.0, # 50 price units below entry
49
+ relative_take_profit=100.0, # 100 price units above entry
50
+ )
51
+
39
52
  # Stop order
40
53
  stop_order = NewOrderRequest(
41
54
  symbol_id=270,
42
55
  order_type=OrderType.STOP,
43
56
  side=OrderSide.SELL,
44
- volume=100,
57
+ volume=symbol.lots_to_volume(0.1),
45
58
  stop_price=4900.0,
46
59
  )
47
60
  ```
@@ -59,16 +72,22 @@ stop_order = NewOrderRequest(
59
72
  ```python
60
73
  from ctrader_api_client.models import ClosePositionRequest
61
74
 
75
+ # Get position to know its volume
76
+ positions = await client.trading.get_open_positions(account_id)
77
+ position = positions[0]
78
+
62
79
  # Close entire position
63
80
  close_all = ClosePositionRequest(
64
- position_id=123456,
65
- volume=100, # Must match position volume for full close
81
+ position_id=position.position_id,
82
+ volume=position.volume, # Must match position volume for full close
66
83
  )
67
84
 
68
- # Partial close
85
+ # Partial close - close half of the position
86
+ symbol = await client.symbols.get_by_id(account_id, position.symbol_id)
87
+ current_lots = symbol.volume_to_lots(position.volume)
69
88
  partial_close = ClosePositionRequest(
70
- position_id=123456,
71
- volume=50, # Close half
89
+ position_id=position.position_id,
90
+ volume=symbol.lots_to_volume(current_lots / 2), # Close half
72
91
  )
73
92
  ```
74
93
 
@@ -128,34 +147,46 @@ partial_close = ClosePositionRequest(
128
147
  options:
129
148
  show_source: false
130
149
 
131
- ## Volume and Price Conversion
150
+ ## Volume Conversion
132
151
 
133
- Volumes in the cTrader API are expressed in **cents** (1/100 of the base unit):
152
+ Volumes in the cTrader API are expressed in **cents** relative to the symbol's `lot_size`:
134
153
 
135
- - `100` = 0.01 lots
136
- - `10000` = 0.1 lots
137
- - `100000` = 1.0 lot
154
+ - For standard forex (lot_size=100000): `100000` = 1.0 lots, `10000` = 0.1 lots, `1000` = 0.01 lots
155
+ - For other instruments, lot_size may vary
138
156
 
139
157
  Use the `Symbol` model for conversions:
140
158
 
141
159
  ```python
142
160
  symbol = await client.symbols.get_by_id(account_id, 270)
143
161
 
144
- # Convert volume to lots
145
- volume_in_cents = 100
146
- lots = symbol.volume_to_lots(volume_in_cents) # 1.0
162
+ # Convert lots to volume for orders
163
+ volume = symbol.lots_to_volume(0.1) # Returns volume in cents
147
164
 
148
- # Convert lots to volume
149
- lots = 0.5
150
- volume = symbol.lots_to_volume(lots) # 50
165
+ # Convert volume to lots for display
166
+ lots = symbol.volume_to_lots(position.volume) # Returns lots as float
151
167
  ```
152
168
 
153
- Prices are returned as raw integers. Use the symbol's `digits` for conversion:
169
+ ## Price Values
154
170
 
155
- ```python
156
- # Raw price from event
157
- raw_price = event.bid # e.g., 123456
171
+ Prices in events and models (bid, ask, OHLC, execution prices, etc.) are returned as **floats** - no conversion needed:
158
172
 
159
- # Convert to decimal
160
- decimal_price = Decimal(raw_price) / Decimal(10 ** 5) # 1.23456
173
+ ```python
174
+ @client.on(SpotEvent)
175
+ async def on_price(event: SpotEvent):
176
+ # bid and ask are already floats
177
+ spread = event.ask - event.bid
178
+ print(f"Spread: {spread}")
179
+
180
+ # Trendbar OHLC are floats
181
+ for bar in trendbars:
182
+ range_size = bar.high - bar.low
183
+ print(f"Range: {range_size}")
184
+
185
+ # Account balance is a float
186
+ account = await client.accounts.get_trader(account_id)
187
+ print(f"Balance: {account.balance}")
188
+
189
+ # Position values are floats
190
+ for pos in positions:
191
+ print(f"Swap: {pos.swap}, Commission: {pos.commission}")
161
192
  ```
@@ -24,7 +24,7 @@ Access via `client.symbols`.
24
24
  symbols = await client.symbols.list_all(account_id)
25
25
 
26
26
  for sym in symbols:
27
- print(f"{sym.symbol_id}: {sym.symbol_name}")
27
+ print(f"{sym.symbol_id}: {sym.name}")
28
28
  ```
29
29
 
30
30
  ### Get Symbol by ID
@@ -33,7 +33,6 @@ for sym in symbols:
33
33
  # Get full symbol details
34
34
  symbol = await client.symbols.get_by_id(account_id, 270)
35
35
 
36
- print(f"Name: {symbol.symbol_name}")
37
36
  print(f"Digits: {symbol.digits}")
38
37
  print(f"Lot size: {symbol.lot_size}")
39
38
  print(f"Min volume: {symbol.min_volume}")
@@ -46,7 +45,7 @@ print(f"Max volume: {symbol.max_volume}")
46
45
  symbols = await client.symbols.get_by_ids(account_id, [270, 271, 272])
47
46
 
48
47
  for sym in symbols:
49
- print(f"{sym.symbol_name}: {sym.digits} digits")
48
+ print(f"{sym.name}: {sym.digits} digits, lot_size={sym.lot_size}")
50
49
  ```
51
50
 
52
51
  ### Search Symbols
@@ -56,36 +55,31 @@ for sym in symbols:
56
55
  eur_pairs = await client.symbols.search(account_id, "EUR")
57
56
 
58
57
  for sym in eur_pairs:
59
- print(sym.symbol_name)
58
+ print(sym.name)
60
59
  ```
61
60
 
62
- ## Price Conversion
61
+ ## Volume Conversion
63
62
 
64
- Prices in the cTrader API are represented as integers (e.g. 12345 for 1.2345) and the decimal place varies from one symbol to another.
65
- Use `Symbol` methods to convert between price and decimal:
63
+ The cTrader API uses volume in "cents" (smallest volume units). The relationship between lots and volume depends on the symbol's `lot_size`:
66
64
 
67
65
  ```python
68
66
  symbol = await client.symbols.get_by_id(account_id, 270)
69
67
 
70
- # Price to decimal
71
- decimal_price = symbol.price_to_decimal(12345) # 1.2345
72
-
73
- # Decimal to price
74
- price = symbol.decimal_to_price(1.2345) # 12345
68
+ # Lots to volume (for placing orders)
69
+ volume = symbol.lots_to_volume(1.0) # e.g., 10000000 for standard forex
75
70
 
71
+ # Volume to lots (for display)
72
+ lots = symbol.volume_to_lots(10000000) # e.g., 1.0 for standard forex
76
73
  ```
77
74
 
78
-
79
- ## Volume Conversion
80
-
81
- Use `Symbol` methods to convert between lots and volume:
75
+ **Note:** Different instruments have different lot sizes. Always use the symbol's methods for conversion:
82
76
 
83
77
  ```python
84
- symbol = await client.symbols.get_by_id(account_id, 270)
85
-
86
- # Lots to volume (cents)
87
- volume = symbol.lots_to_volume(1.0) # 100000
78
+ # Standard forex (lot_size=100000)
79
+ forex_symbol = await client.symbols.get_by_id(account_id, 1) # EURUSD (lot_size=10000000)
80
+ forex_volume = forex_symbol.lots_to_volume(0.1) # 1000000
88
81
 
89
- # Volume to lots
90
- lots = symbol.volume_to_lots(100000) # 1.0
82
+ # Index CFD might have different lot_size
83
+ index_symbol = await client.symbols.get_by_id(account_id, 270) # US500 (lot_size=100)
84
+ index_volume = index_symbol.lots_to_volume(0.1) # 10
91
85
  ```
@@ -28,11 +28,15 @@ Access via `client.trading`.
28
28
  from ctrader_api_client.models import NewOrderRequest
29
29
  from ctrader_api_client.enums import OrderType, OrderSide
30
30
 
31
+ # Get symbol info for volume conversion
32
+ symbol = await client.symbols.get_by_id(account_id, 270)
33
+
34
+ # Place a 0.1 lot buy order
31
35
  request = NewOrderRequest(
32
36
  symbol_id=270,
33
37
  order_type=OrderType.MARKET,
34
38
  side=OrderSide.BUY,
35
- volume=100, # 0.01 lots (volume in cents)
39
+ volume=symbol.lots_to_volume(0.1), # Convert lots to volume
36
40
  )
37
41
 
38
42
  result = await client.trading.place_order(account_id, request)
@@ -42,19 +46,38 @@ print(f"Order {result.order_id}: {result.execution_type}")
42
46
  ### Place a Limit Order
43
47
 
44
48
  ```python
49
+ # Get symbol info
50
+ symbol = await client.symbols.get_by_id(account_id, 270)
51
+
45
52
  request = NewOrderRequest(
46
53
  symbol_id=270,
47
54
  order_type=OrderType.LIMIT,
48
55
  side=OrderSide.BUY,
49
- volume=100,
56
+ volume=symbol.lots_to_volume(0.1),
50
57
  limit_price=5000.0, # Limit price
51
- stop_loss=4950.0, # Optional SL
58
+ stop_loss=4950.0, # Optional SL
52
59
  take_profit=5100.0, # Optional TP
53
60
  )
54
61
 
55
62
  result = await client.trading.place_order(account_id, request)
56
63
  ```
57
64
 
65
+ ### Place an Order with Relative SL/TP
66
+
67
+ ```python
68
+ # Relative SL/TP are specified in price units (distance from entry)
69
+ request = NewOrderRequest(
70
+ symbol_id=270,
71
+ order_type=OrderType.MARKET,
72
+ side=OrderSide.BUY,
73
+ volume=symbol.lots_to_volume(0.1),
74
+ relative_stop_loss=50.0, # 50 points below entry
75
+ relative_take_profit=100.0, # 100 points above entry
76
+ )
77
+
78
+ result = await client.trading.place_order(account_id, request)
79
+ ```
80
+
58
81
  ### Cancel an Order
59
82
 
60
83
  ```python
@@ -67,9 +90,30 @@ print(f"Cancelled: {result.execution_type}")
67
90
  ```python
68
91
  from ctrader_api_client.models import ClosePositionRequest
69
92
 
93
+ # First, get the position to know its volume
94
+ positions = await client.trading.get_open_positions(account_id)
95
+ position = next(p for p in positions if p.position_id == position_id)
96
+
97
+ # Close the entire position
70
98
  request = ClosePositionRequest(
71
- position_id=123456,
72
- volume=100, # Close 0.01 lots (or full volume)
99
+ position_id=position.position_id,
100
+ volume=position.volume, # Use full volume for complete close
101
+ )
102
+
103
+ result = await client.trading.close_position(account_id, request)
104
+ print(f"Position closed: {result.execution_type}")
105
+ ```
106
+
107
+ ### Partial Close a Position
108
+
109
+ ```python
110
+ # Get symbol to convert lots
111
+ symbol = await client.symbols.get_by_id(account_id, position.symbol_id)
112
+
113
+ # Close half of a 1-lot position
114
+ request = ClosePositionRequest(
115
+ position_id=position.position_id,
116
+ volume=symbol.lots_to_volume(0.5), # Close 0.5 lots
73
117
  )
74
118
 
75
119
  result = await client.trading.close_position(account_id, request)
@@ -95,7 +139,12 @@ result = await client.trading.amend_position(account_id, request)
95
139
  positions = await client.trading.get_open_positions(account_id)
96
140
 
97
141
  for pos in positions:
98
- print(f"Position {pos.position_id}: {pos.volume} @ {pos.entry_price}")
142
+ # Get symbol for volume conversion
143
+ symbol = await client.symbols.get_by_id(account_id, pos.symbol_id)
144
+ lots = symbol.volume_to_lots(pos.volume)
145
+
146
+ print(f"Position {pos.position_id}: {lots} lots @ {pos.entry_price}")
147
+ print(f" Swap: {pos.swap}, Commission: {pos.commission}")
99
148
  ```
100
149
 
101
150
  ### Get Pending Orders
@@ -120,4 +169,9 @@ deals = await client.trading.get_deals(
120
169
 
121
170
  for deal in deals:
122
171
  print(f"Deal {deal.deal_id}: {deal.side} {deal.filled_volume}")
172
+ print(f" Commission: {deal.commission}")
173
+
174
+ # Check if this deal closed a position
175
+ if deal.is_closing_deal and deal.close_detail:
176
+ print(f" Gross P/L: {deal.close_detail.gross_profit}")
123
177
  ```
@@ -85,11 +85,14 @@ await client.market_data.subscribe_spots(creds.account_id, [270])
85
85
  from ctrader_api_client.models import NewOrderRequest
86
86
  from ctrader_api_client.enums import OrderType, OrderSide
87
87
 
88
+ # Get symbol info for volume conversion
89
+ symbol = await client.symbols.get_by_id(creds.account_id, 270)
90
+
88
91
  request = NewOrderRequest(
89
92
  symbol_id=270,
90
93
  order_type=OrderType.MARKET,
91
94
  side=OrderSide.BUY,
92
- volume=100, # Volume in cents (100 = 0.01 lots)
95
+ volume=symbol.lots_to_volume(0.01), # Convert 0.01 lots to volume
93
96
  )
94
97
 
95
98
  result = await client.trading.place_order(creds.account_id, request)
@@ -131,9 +134,6 @@ async def on_execution(event: ExecutionEvent):
131
134
  """Called when orders are executed."""
132
135
  print(f"Execution: {event.execution_type} for order {event.order_id}")
133
136
 
134
- if event.is_closing_deal:
135
- print(f"Position closed. Profit: {event.close_detail.get_net_profit()}")
136
-
137
137
 
138
138
  async def main():
139
139
  async with client:
@@ -145,12 +145,15 @@ async def main():
145
145
  expires_at=1778617423,
146
146
  )
147
147
 
148
+ # Get symbol for volume conversion
149
+ symbol = await client.symbols.get_by_id(creds.account_id, 270)
150
+
148
151
  # Place a test order
149
152
  order = NewOrderRequest(
150
153
  symbol_id=270,
151
154
  order_type=OrderType.MARKET,
152
155
  side=OrderSide.BUY,
153
- volume=100,
156
+ volume=symbol.lots_to_volume(0.01), # 0.01 lots
154
157
  )
155
158
  await client.trading.place_order(creds.account_id, order)
156
159
 
@@ -203,7 +206,7 @@ config = ClientConfig(
203
206
 
204
207
  # Timeouts
205
208
  heartbeat_interval=10.0,
206
- heartbeat_timeout=30.0, # Or 0 to disable server heartbeat checks
209
+ heartbeat_timeout=0, # 0 to disable server heartbeat checks (default)
207
210
  request_timeout=30.0,
208
211
 
209
212
  # Reconnection
@@ -41,6 +41,7 @@ client = CTraderClient(config)
41
41
 
42
42
  @client.on(SpotEvent, symbol_id=270) # Filter by symbol
43
43
  async def on_price(event: SpotEvent):
44
+ # bid and ask are floats
44
45
  print(f"Price update: {event.bid}/{event.ask}")
45
46
 
46
47
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctrader-api-client"
3
- version = "0.1.2"
3
+ version = "0.2.0"
4
4
  description = "API Client to interact with the cTrader Open API spec"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -33,7 +33,7 @@ dev = [
33
33
  "mkdocstrings[python]>=1.0.3",
34
34
  "pytest>=9.0.3",
35
35
  "ruff>=0.15.9",
36
- "ty>=0.0.28",
36
+ "zuban>=0.7.0",
37
37
  ]
38
38
 
39
39
  [tool.ruff]
@@ -68,8 +68,7 @@ line-ending = "lf"
68
68
  skip-magic-trailing-comma = false
69
69
  docstring-code-format = true
70
70
 
71
- [[tool.ty.overrides]]
72
- include = ["tests/**"]
73
71
 
74
- [tool.ty.overrides.rules]
75
- invalid-assignment = "ignore"
72
+ [[tool.mypy.overrides]]
73
+ module = "tests.*"
74
+ disable_error_code = ["method-assign"]
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Sequence
3
4
  from datetime import datetime
4
5
  from typing import TYPE_CHECKING
5
6
 
@@ -177,7 +178,10 @@ class MarketDataAPI:
177
178
  ) -> None:
178
179
  """Subscribe to live trendbar (candle) updates.
179
180
 
180
- After subscribing, trendbar events will be delivered via the event system.
181
+ Requires subscribing to spots for the same symbol beforehand.
182
+
183
+ After subscribing, trendbar data will be delivered via the event system inside the SpotEvent object.
184
+ Use `@client.on(SpotEvent)` to handle them.
181
185
 
182
186
  Args:
183
187
  account_id: The cTID trader account ID.
@@ -378,7 +382,7 @@ class MarketDataAPI:
378
382
  to_timestamp: datetime,
379
383
  quote_type: str = "BID",
380
384
  timeout: float | None = None,
381
- ) -> list[TickData]:
385
+ ) -> Sequence[TickData]:
382
386
  """Get historical tick data.
383
387
 
384
388
  Args:
@@ -390,7 +394,7 @@ class MarketDataAPI:
390
394
  timeout: Request timeout (uses default if None).
391
395
 
392
396
  Returns:
393
- List of TickData objects, ordered by timestamp ascending.
397
+ List of TickData objects, ordered by newest first.
394
398
 
395
399
  Note:
396
400
  Tick data can be voluminous. Use small time windows to avoid
@@ -421,4 +425,4 @@ class MarketDataAPI:
421
425
  description=f"Expected ProtoOAGetTickDataRes, got {type(response).__name__}",
422
426
  )
423
427
 
424
- return [TickData.from_proto(t) for t in response.tick_data]
428
+ return TickData.from_proto_list(response.tick_data)