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.
- ctrader_api_client-0.2.0/.claude/settings.local.json +7 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/Justfile +1 -1
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/PKG-INFO +1 -1
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/accounts.md +1 -1
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/events.md +20 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/market-data.md +33 -7
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/models.md +56 -25
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/symbols.md +16 -22
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/trading.md +60 -6
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/getting-started.md +9 -6
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/index.md +1 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/pyproject.toml +5 -6
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/api/market_data.py +8 -4
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/api/trading.py +80 -3
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/client.py +2 -2
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/events/router.py +4 -2
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/events/types.py +7 -8
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/account.py +14 -24
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/deal.py +26 -91
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/market_data.py +57 -94
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/order.py +0 -63
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/position.py +9 -72
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/requests.py +16 -17
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/symbol.py +5 -30
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/_internal/test_messages.py +2 -2
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/api/test_accounts.py +1 -2
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/api/test_market_data_api.py +1 -1
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/events/test_emitter.py +24 -16
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/events/test_router.py +7 -3
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/events/test_types.py +11 -7
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_account.py +12 -52
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_deal.py +57 -195
- ctrader_api_client-0.2.0/tests/unit/models/test_market_data.py +159 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_order.py +1 -130
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_position.py +8 -168
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_requests.py +3 -2
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/models/test_symbol.py +39 -86
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/uv.lock +22 -27
- ctrader_api_client-0.1.2/tests/unit/models/test_market_data.py +0 -265
- ctrader_api_client-0.1.2/tests/unit/test_config.py +0 -149
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/.github/workflows/docs.yml +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/.gitignore +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/.pre-commit-config.yaml +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/.python-version +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/LICENSE +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/README.md +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/client.md +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/docs/api/enums.md +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/mkdocs.yml +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/SOURCE +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/VERSION +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/update.sh +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/vendor/.gitkeep +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/vendor/OpenApiCommonMessages.proto +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/vendor/OpenApiCommonModelMessages.proto +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/vendor/OpenApiMessages.proto +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/protos/vendor/OpenApiModelMessages.proto +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/scripts/fix_proto_imports.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/__init__.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/__init__.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/messages.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonMessages.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/proto/OpenApiCommonModelMessages.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/proto/OpenApiMessages.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/proto/OpenApiModelMessages.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/proto/__init__.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/_internal/serialization.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/api/__init__.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/api/accounts.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/api/symbols.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/auth/__init__.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/auth/credentials.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/auth/manager.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/config.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/connection/__init__.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/connection/heartbeat.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/connection/protocol.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/connection/transport.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/enums.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/events/__init__.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/events/emitter.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/exceptions.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/__init__.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/models/_base.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/py.typed +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/_internal/test_serialization.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/api/conftest.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/api/test_symbols.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/api/test_trading.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/auth/test_credentials.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/auth/test_manager.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/connection/test_heartbeat.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/connection/test_protocol.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/connection/test_transport.py +0 -0
- {ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/tests/unit/test_client.py +0 -0
|
@@ -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
|
|
29
|
+
uv run zuban check {{directory}}
|
|
30
30
|
|
|
31
31
|
# Run tests using pytest, optionally specifying a directory to test.
|
|
32
32
|
test directory='':
|
|
@@ -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.
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
65
|
-
volume=
|
|
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=
|
|
71
|
-
volume=
|
|
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
|
|
150
|
+
## Volume Conversion
|
|
132
151
|
|
|
133
|
-
Volumes in the cTrader API are expressed in **cents**
|
|
152
|
+
Volumes in the cTrader API are expressed in **cents** relative to the symbol's `lot_size`:
|
|
134
153
|
|
|
135
|
-
- `
|
|
136
|
-
-
|
|
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
|
|
145
|
-
|
|
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
|
|
149
|
-
lots =
|
|
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
|
-
|
|
169
|
+
## Price Values
|
|
154
170
|
|
|
155
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
58
|
+
print(sym.name)
|
|
60
59
|
```
|
|
61
60
|
|
|
62
|
-
##
|
|
61
|
+
## Volume Conversion
|
|
63
62
|
|
|
64
|
-
|
|
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
|
-
#
|
|
71
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
#
|
|
90
|
-
|
|
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=
|
|
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=
|
|
56
|
+
volume=symbol.lots_to_volume(0.1),
|
|
50
57
|
limit_price=5000.0, # Limit price
|
|
51
|
-
stop_loss=4950.0,
|
|
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=
|
|
72
|
-
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
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
209
|
+
heartbeat_timeout=0, # 0 to disable server heartbeat checks (default)
|
|
207
210
|
request_timeout=30.0,
|
|
208
211
|
|
|
209
212
|
# Reconnection
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ctrader-api-client"
|
|
3
|
-
version = "0.
|
|
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
|
-
"
|
|
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.
|
|
75
|
-
|
|
72
|
+
[[tool.mypy.overrides]]
|
|
73
|
+
module = "tests.*"
|
|
74
|
+
disable_error_code = ["method-assign"]
|
{ctrader_api_client-0.1.2 → ctrader_api_client-0.2.0}/src/ctrader_api_client/api/market_data.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
) ->
|
|
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
|
|
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
|
|
428
|
+
return TickData.from_proto_list(response.tick_data)
|