topstep-client-py 0.1.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.
- topstep_client_py-0.1.0/LICENSE +21 -0
- topstep_client_py-0.1.0/PKG-INFO +233 -0
- topstep_client_py-0.1.0/README.md +205 -0
- topstep_client_py-0.1.0/pyproject.toml +43 -0
- topstep_client_py-0.1.0/setup.cfg +4 -0
- topstep_client_py-0.1.0/src/topstep/__init__.py +55 -0
- topstep_client_py-0.1.0/src/topstep/auth.py +50 -0
- topstep_client_py-0.1.0/src/topstep/client.py +118 -0
- topstep_client_py-0.1.0/src/topstep/endpoints/__init__.py +17 -0
- topstep_client_py-0.1.0/src/topstep/endpoints/accounts.py +18 -0
- topstep_client_py-0.1.0/src/topstep/endpoints/contracts.py +31 -0
- topstep_client_py-0.1.0/src/topstep/endpoints/history.py +48 -0
- topstep_client_py-0.1.0/src/topstep/endpoints/orders.py +75 -0
- topstep_client_py-0.1.0/src/topstep/endpoints/positions.py +33 -0
- topstep_client_py-0.1.0/src/topstep/endpoints/trades.py +31 -0
- topstep_client_py-0.1.0/src/topstep/exceptions.py +32 -0
- topstep_client_py-0.1.0/src/topstep/http.py +102 -0
- topstep_client_py-0.1.0/src/topstep/models/__init__.py +31 -0
- topstep_client_py-0.1.0/src/topstep/models/account.py +13 -0
- topstep_client_py-0.1.0/src/topstep/models/bar.py +26 -0
- topstep_client_py-0.1.0/src/topstep/models/contract.py +15 -0
- topstep_client_py-0.1.0/src/topstep/models/order.py +82 -0
- topstep_client_py-0.1.0/src/topstep/models/position.py +24 -0
- topstep_client_py-0.1.0/src/topstep/models/trade.py +22 -0
- topstep_client_py-0.1.0/src/topstep/py.typed +0 -0
- topstep_client_py-0.1.0/src/topstep/realtime/__init__.py +6 -0
- topstep_client_py-0.1.0/src/topstep/realtime/market_hub.py +236 -0
- topstep_client_py-0.1.0/src/topstep/realtime/user_hub.py +257 -0
- topstep_client_py-0.1.0/src/topstep_client_py.egg-info/PKG-INFO +233 -0
- topstep_client_py-0.1.0/src/topstep_client_py.egg-info/SOURCES.txt +36 -0
- topstep_client_py-0.1.0/src/topstep_client_py.egg-info/dependency_links.txt +1 -0
- topstep_client_py-0.1.0/src/topstep_client_py.egg-info/requires.txt +9 -0
- topstep_client_py-0.1.0/src/topstep_client_py.egg-info/top_level.txt +1 -0
- topstep_client_py-0.1.0/tests/test_accounts.py +41 -0
- topstep_client_py-0.1.0/tests/test_auth.py +52 -0
- topstep_client_py-0.1.0/tests/test_client.py +122 -0
- topstep_client_py-0.1.0/tests/test_http.py +65 -0
- topstep_client_py-0.1.0/tests/test_orders.py +90 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Icarus Research
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: topstep-client-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight async Python client for the TopstepX / ProjectX Gateway API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: topstep,projectx,trading,futures,api,propfirm
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0.0
|
|
21
|
+
Requires-Dist: signalrcore>=0.9.5
|
|
22
|
+
Requires-Dist: websocket-client>=0.54.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
26
|
+
Requires-Dist: respx>=0.22; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# topstep-client-py
|
|
30
|
+
|
|
31
|
+
Lightweight async Python client for the [TopstepX](https://www.topstep.com/) / [ProjectX Gateway API](https://gateway.docs.projectx.com/docs/intro/).
|
|
32
|
+
|
|
33
|
+
## Disclaimer
|
|
34
|
+
|
|
35
|
+
This project is an independent community-built library. It is not officially affiliated with, endorsed by, maintained by, or otherwise directly associated with Topstep, TopstepX, or ProjectX.
|
|
36
|
+
|
|
37
|
+
The goal of this package is to provide a clean and practical Python client for the public API and realtime interfaces. It is maintained on a best-effort basis and will be kept up to date as quickly as reasonably possible when the upstream API changes.
|
|
38
|
+
|
|
39
|
+
This repository only provides the API client layer. It does not include trading strategies, alpha generation, signal logic, backtesting frameworks, portfolio tooling, or other trader-specific systems. Those decisions and tools are intentionally left to each user and their own workflow.
|
|
40
|
+
|
|
41
|
+
- Async-first (built on `httpx`)
|
|
42
|
+
- Fully typed responses with Pydantic models
|
|
43
|
+
- All 16 REST endpoints covered
|
|
44
|
+
- Real-time market & user data via SignalR WebSocket
|
|
45
|
+
- Retry with backoff + rate limit handling
|
|
46
|
+
- Clean exception hierarchy
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install topstep-client-py
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import asyncio
|
|
58
|
+
from datetime import datetime, timedelta, timezone
|
|
59
|
+
from topstep import TopstepClient, BarUnit
|
|
60
|
+
|
|
61
|
+
async def main():
|
|
62
|
+
async with await TopstepClient.create(
|
|
63
|
+
username="you@email.com",
|
|
64
|
+
api_key="your-api-key",
|
|
65
|
+
) as client:
|
|
66
|
+
|
|
67
|
+
# Search accounts
|
|
68
|
+
accounts = await client.accounts.search()
|
|
69
|
+
account = accounts[0]
|
|
70
|
+
print(f"Account: {account.name} | Balance: {account.balance}")
|
|
71
|
+
|
|
72
|
+
# Find a contract
|
|
73
|
+
contracts = await client.contracts.search("Micro E-mini Nasdaq")
|
|
74
|
+
contract = contracts[0]
|
|
75
|
+
print(f"Contract: {contract.description} | Tick: {contract.tick_size}")
|
|
76
|
+
|
|
77
|
+
# Get historical bars
|
|
78
|
+
end = datetime.now(timezone.utc)
|
|
79
|
+
start = end - timedelta(hours=1)
|
|
80
|
+
bars = await client.history.retrieve_bars(
|
|
81
|
+
contract_id=contract.id,
|
|
82
|
+
start=start,
|
|
83
|
+
end=end,
|
|
84
|
+
unit=BarUnit.MINUTE,
|
|
85
|
+
unit_number=5,
|
|
86
|
+
)
|
|
87
|
+
for bar in bars[-3:]:
|
|
88
|
+
print(f" {bar.timestamp} O:{bar.open} H:{bar.high} L:{bar.low} C:{bar.close}")
|
|
89
|
+
|
|
90
|
+
asyncio.run(main())
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Placing Orders
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from topstep import PlaceOrderRequest, OrderType, OrderSide, Bracket
|
|
97
|
+
|
|
98
|
+
order_id = await client.orders.place(PlaceOrderRequest(
|
|
99
|
+
account_id=account.id,
|
|
100
|
+
contract_id=contract.id,
|
|
101
|
+
type=OrderType.STOP,
|
|
102
|
+
side=OrderSide.BUY,
|
|
103
|
+
size=2,
|
|
104
|
+
stop_price=21500.0,
|
|
105
|
+
stop_loss_bracket=Bracket(ticks=-20, type=4),
|
|
106
|
+
take_profit_bracket=Bracket(ticks=40, type=1),
|
|
107
|
+
))
|
|
108
|
+
print(f"Order placed: {order_id}")
|
|
109
|
+
|
|
110
|
+
# Check open orders
|
|
111
|
+
open_orders = await client.orders.search_open(account.id)
|
|
112
|
+
|
|
113
|
+
# Cancel an order
|
|
114
|
+
await client.orders.cancel(account.id, order_id)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Positions
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# Get open positions
|
|
121
|
+
positions = await client.positions.search_open(account.id)
|
|
122
|
+
|
|
123
|
+
# Close all positions for a contract
|
|
124
|
+
await client.positions.close(account.id, contract.id)
|
|
125
|
+
|
|
126
|
+
# Partial close
|
|
127
|
+
await client.positions.partial_close(account.id, contract.id, size=1)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Trade History
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from datetime import datetime, timezone
|
|
134
|
+
|
|
135
|
+
trades = await client.trades.search(
|
|
136
|
+
account_id=account.id,
|
|
137
|
+
start=datetime(2026, 3, 1, tzinfo=timezone.utc),
|
|
138
|
+
)
|
|
139
|
+
for trade in trades:
|
|
140
|
+
print(f" {trade.price} | P&L: {trade.profit_and_loss} | Fees: {trade.fees}")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Real-Time Market Data (WebSocket)
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import asyncio
|
|
147
|
+
from topstep import TopstepClient
|
|
148
|
+
|
|
149
|
+
async def main():
|
|
150
|
+
async with await TopstepClient.create(
|
|
151
|
+
username="you@email.com",
|
|
152
|
+
api_key="your-api-key",
|
|
153
|
+
) as client:
|
|
154
|
+
|
|
155
|
+
# Register callbacks
|
|
156
|
+
client.market.on_quote(lambda *args: print("QUOTE:", args))
|
|
157
|
+
client.market.on_trade(lambda *args: print("TRADE:", args))
|
|
158
|
+
client.market.on_depth(lambda *args: print("DEPTH:", args))
|
|
159
|
+
|
|
160
|
+
# Connect and subscribe
|
|
161
|
+
await client.market.connect()
|
|
162
|
+
await client.market.subscribe_all("CON.F.US.ENQ.H26")
|
|
163
|
+
|
|
164
|
+
# Keep alive
|
|
165
|
+
try:
|
|
166
|
+
while True:
|
|
167
|
+
await asyncio.sleep(1)
|
|
168
|
+
except KeyboardInterrupt:
|
|
169
|
+
await client.market.stop()
|
|
170
|
+
|
|
171
|
+
asyncio.run(main())
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Real-Time User Data (WebSocket)
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
async with await TopstepClient.create(...) as client:
|
|
178
|
+
|
|
179
|
+
client.user.on_order(lambda *args: print("ORDER:", args))
|
|
180
|
+
client.user.on_position(lambda *args: print("POSITION:", args))
|
|
181
|
+
client.user.on_trade(lambda *args: print("TRADE:", args))
|
|
182
|
+
client.user.on_account(lambda *args: print("ACCOUNT:", args))
|
|
183
|
+
|
|
184
|
+
await client.user.connect()
|
|
185
|
+
await client.user.subscribe_all(account_id=account.id)
|
|
186
|
+
|
|
187
|
+
# ...
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Token Refresh
|
|
191
|
+
|
|
192
|
+
Tokens expire after 24 hours. Refresh manually:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
await client.refresh_token()
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Error Handling
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from topstep import TopstepError, AuthenticationError, APIError, RateLimitError
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
accounts = await client.accounts.search()
|
|
205
|
+
except AuthenticationError:
|
|
206
|
+
# Invalid credentials or expired token
|
|
207
|
+
await client.refresh_token()
|
|
208
|
+
except RateLimitError:
|
|
209
|
+
# HTTP 429 — too many requests (auto-retried 3 times before raising)
|
|
210
|
+
pass
|
|
211
|
+
except APIError as e:
|
|
212
|
+
# API returned success=False
|
|
213
|
+
print(f"API error [{e.error_code}]: {e}")
|
|
214
|
+
except TopstepError:
|
|
215
|
+
# Any other client error
|
|
216
|
+
pass
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Rate Limits
|
|
220
|
+
|
|
221
|
+
The API enforces these limits (handled automatically with retry + backoff):
|
|
222
|
+
|
|
223
|
+
- `History/retrieveBars`: 50 requests per 30 seconds
|
|
224
|
+
- All other endpoints: 200 requests per 60 seconds
|
|
225
|
+
|
|
226
|
+
## Development
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
git clone https://github.com/YOUR_USER/topstep-client-py.git
|
|
230
|
+
cd topstep-client-py
|
|
231
|
+
pip install -e ".[dev]"
|
|
232
|
+
pytest
|
|
233
|
+
```
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# topstep-client-py
|
|
2
|
+
|
|
3
|
+
Lightweight async Python client for the [TopstepX](https://www.topstep.com/) / [ProjectX Gateway API](https://gateway.docs.projectx.com/docs/intro/).
|
|
4
|
+
|
|
5
|
+
## Disclaimer
|
|
6
|
+
|
|
7
|
+
This project is an independent community-built library. It is not officially affiliated with, endorsed by, maintained by, or otherwise directly associated with Topstep, TopstepX, or ProjectX.
|
|
8
|
+
|
|
9
|
+
The goal of this package is to provide a clean and practical Python client for the public API and realtime interfaces. It is maintained on a best-effort basis and will be kept up to date as quickly as reasonably possible when the upstream API changes.
|
|
10
|
+
|
|
11
|
+
This repository only provides the API client layer. It does not include trading strategies, alpha generation, signal logic, backtesting frameworks, portfolio tooling, or other trader-specific systems. Those decisions and tools are intentionally left to each user and their own workflow.
|
|
12
|
+
|
|
13
|
+
- Async-first (built on `httpx`)
|
|
14
|
+
- Fully typed responses with Pydantic models
|
|
15
|
+
- All 16 REST endpoints covered
|
|
16
|
+
- Real-time market & user data via SignalR WebSocket
|
|
17
|
+
- Retry with backoff + rate limit handling
|
|
18
|
+
- Clean exception hierarchy
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install topstep-client-py
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import asyncio
|
|
30
|
+
from datetime import datetime, timedelta, timezone
|
|
31
|
+
from topstep import TopstepClient, BarUnit
|
|
32
|
+
|
|
33
|
+
async def main():
|
|
34
|
+
async with await TopstepClient.create(
|
|
35
|
+
username="you@email.com",
|
|
36
|
+
api_key="your-api-key",
|
|
37
|
+
) as client:
|
|
38
|
+
|
|
39
|
+
# Search accounts
|
|
40
|
+
accounts = await client.accounts.search()
|
|
41
|
+
account = accounts[0]
|
|
42
|
+
print(f"Account: {account.name} | Balance: {account.balance}")
|
|
43
|
+
|
|
44
|
+
# Find a contract
|
|
45
|
+
contracts = await client.contracts.search("Micro E-mini Nasdaq")
|
|
46
|
+
contract = contracts[0]
|
|
47
|
+
print(f"Contract: {contract.description} | Tick: {contract.tick_size}")
|
|
48
|
+
|
|
49
|
+
# Get historical bars
|
|
50
|
+
end = datetime.now(timezone.utc)
|
|
51
|
+
start = end - timedelta(hours=1)
|
|
52
|
+
bars = await client.history.retrieve_bars(
|
|
53
|
+
contract_id=contract.id,
|
|
54
|
+
start=start,
|
|
55
|
+
end=end,
|
|
56
|
+
unit=BarUnit.MINUTE,
|
|
57
|
+
unit_number=5,
|
|
58
|
+
)
|
|
59
|
+
for bar in bars[-3:]:
|
|
60
|
+
print(f" {bar.timestamp} O:{bar.open} H:{bar.high} L:{bar.low} C:{bar.close}")
|
|
61
|
+
|
|
62
|
+
asyncio.run(main())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Placing Orders
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from topstep import PlaceOrderRequest, OrderType, OrderSide, Bracket
|
|
69
|
+
|
|
70
|
+
order_id = await client.orders.place(PlaceOrderRequest(
|
|
71
|
+
account_id=account.id,
|
|
72
|
+
contract_id=contract.id,
|
|
73
|
+
type=OrderType.STOP,
|
|
74
|
+
side=OrderSide.BUY,
|
|
75
|
+
size=2,
|
|
76
|
+
stop_price=21500.0,
|
|
77
|
+
stop_loss_bracket=Bracket(ticks=-20, type=4),
|
|
78
|
+
take_profit_bracket=Bracket(ticks=40, type=1),
|
|
79
|
+
))
|
|
80
|
+
print(f"Order placed: {order_id}")
|
|
81
|
+
|
|
82
|
+
# Check open orders
|
|
83
|
+
open_orders = await client.orders.search_open(account.id)
|
|
84
|
+
|
|
85
|
+
# Cancel an order
|
|
86
|
+
await client.orders.cancel(account.id, order_id)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Positions
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# Get open positions
|
|
93
|
+
positions = await client.positions.search_open(account.id)
|
|
94
|
+
|
|
95
|
+
# Close all positions for a contract
|
|
96
|
+
await client.positions.close(account.id, contract.id)
|
|
97
|
+
|
|
98
|
+
# Partial close
|
|
99
|
+
await client.positions.partial_close(account.id, contract.id, size=1)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Trade History
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from datetime import datetime, timezone
|
|
106
|
+
|
|
107
|
+
trades = await client.trades.search(
|
|
108
|
+
account_id=account.id,
|
|
109
|
+
start=datetime(2026, 3, 1, tzinfo=timezone.utc),
|
|
110
|
+
)
|
|
111
|
+
for trade in trades:
|
|
112
|
+
print(f" {trade.price} | P&L: {trade.profit_and_loss} | Fees: {trade.fees}")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Real-Time Market Data (WebSocket)
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
import asyncio
|
|
119
|
+
from topstep import TopstepClient
|
|
120
|
+
|
|
121
|
+
async def main():
|
|
122
|
+
async with await TopstepClient.create(
|
|
123
|
+
username="you@email.com",
|
|
124
|
+
api_key="your-api-key",
|
|
125
|
+
) as client:
|
|
126
|
+
|
|
127
|
+
# Register callbacks
|
|
128
|
+
client.market.on_quote(lambda *args: print("QUOTE:", args))
|
|
129
|
+
client.market.on_trade(lambda *args: print("TRADE:", args))
|
|
130
|
+
client.market.on_depth(lambda *args: print("DEPTH:", args))
|
|
131
|
+
|
|
132
|
+
# Connect and subscribe
|
|
133
|
+
await client.market.connect()
|
|
134
|
+
await client.market.subscribe_all("CON.F.US.ENQ.H26")
|
|
135
|
+
|
|
136
|
+
# Keep alive
|
|
137
|
+
try:
|
|
138
|
+
while True:
|
|
139
|
+
await asyncio.sleep(1)
|
|
140
|
+
except KeyboardInterrupt:
|
|
141
|
+
await client.market.stop()
|
|
142
|
+
|
|
143
|
+
asyncio.run(main())
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Real-Time User Data (WebSocket)
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
async with await TopstepClient.create(...) as client:
|
|
150
|
+
|
|
151
|
+
client.user.on_order(lambda *args: print("ORDER:", args))
|
|
152
|
+
client.user.on_position(lambda *args: print("POSITION:", args))
|
|
153
|
+
client.user.on_trade(lambda *args: print("TRADE:", args))
|
|
154
|
+
client.user.on_account(lambda *args: print("ACCOUNT:", args))
|
|
155
|
+
|
|
156
|
+
await client.user.connect()
|
|
157
|
+
await client.user.subscribe_all(account_id=account.id)
|
|
158
|
+
|
|
159
|
+
# ...
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Token Refresh
|
|
163
|
+
|
|
164
|
+
Tokens expire after 24 hours. Refresh manually:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
await client.refresh_token()
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Error Handling
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from topstep import TopstepError, AuthenticationError, APIError, RateLimitError
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
accounts = await client.accounts.search()
|
|
177
|
+
except AuthenticationError:
|
|
178
|
+
# Invalid credentials or expired token
|
|
179
|
+
await client.refresh_token()
|
|
180
|
+
except RateLimitError:
|
|
181
|
+
# HTTP 429 — too many requests (auto-retried 3 times before raising)
|
|
182
|
+
pass
|
|
183
|
+
except APIError as e:
|
|
184
|
+
# API returned success=False
|
|
185
|
+
print(f"API error [{e.error_code}]: {e}")
|
|
186
|
+
except TopstepError:
|
|
187
|
+
# Any other client error
|
|
188
|
+
pass
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Rate Limits
|
|
192
|
+
|
|
193
|
+
The API enforces these limits (handled automatically with retry + backoff):
|
|
194
|
+
|
|
195
|
+
- `History/retrieveBars`: 50 requests per 30 seconds
|
|
196
|
+
- All other endpoints: 200 requests per 60 seconds
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
git clone https://github.com/YOUR_USER/topstep-client-py.git
|
|
202
|
+
cd topstep-client-py
|
|
203
|
+
pip install -e ".[dev]"
|
|
204
|
+
pytest
|
|
205
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "topstep-client-py"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight async Python client for the TopstepX / ProjectX Gateway API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["topstep", "projectx", "trading", "futures", "api", "propfirm"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
"pydantic>=2.0.0",
|
|
27
|
+
"signalrcore>=0.9.5",
|
|
28
|
+
"websocket-client>=0.54.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8.0",
|
|
34
|
+
"pytest-asyncio>=0.24",
|
|
35
|
+
"respx>=0.22",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""TopstepX Python Client — async SDK for the ProjectX Gateway API."""
|
|
2
|
+
|
|
3
|
+
from topstep.client import TopstepClient
|
|
4
|
+
from topstep.exceptions import (
|
|
5
|
+
APIError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
HTTPError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
TopstepError,
|
|
10
|
+
)
|
|
11
|
+
from topstep.models import (
|
|
12
|
+
Account,
|
|
13
|
+
Bar,
|
|
14
|
+
BarUnit,
|
|
15
|
+
Bracket,
|
|
16
|
+
Contract,
|
|
17
|
+
Order,
|
|
18
|
+
OrderSide,
|
|
19
|
+
OrderStatus,
|
|
20
|
+
OrderType,
|
|
21
|
+
PlaceOrderRequest,
|
|
22
|
+
Position,
|
|
23
|
+
PositionType,
|
|
24
|
+
Trade,
|
|
25
|
+
)
|
|
26
|
+
from topstep.realtime import MarketHub, UserHub
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"TopstepClient",
|
|
32
|
+
# Exceptions
|
|
33
|
+
"TopstepError",
|
|
34
|
+
"AuthenticationError",
|
|
35
|
+
"APIError",
|
|
36
|
+
"HTTPError",
|
|
37
|
+
"RateLimitError",
|
|
38
|
+
# Models
|
|
39
|
+
"Account",
|
|
40
|
+
"Bar",
|
|
41
|
+
"BarUnit",
|
|
42
|
+
"Bracket",
|
|
43
|
+
"Contract",
|
|
44
|
+
"Order",
|
|
45
|
+
"OrderSide",
|
|
46
|
+
"OrderStatus",
|
|
47
|
+
"OrderType",
|
|
48
|
+
"PlaceOrderRequest",
|
|
49
|
+
"Position",
|
|
50
|
+
"PositionType",
|
|
51
|
+
"Trade",
|
|
52
|
+
# Realtime
|
|
53
|
+
"MarketHub",
|
|
54
|
+
"UserHub",
|
|
55
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Authentication and token management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from topstep.exceptions import AuthenticationError
|
|
6
|
+
from topstep.http import HTTPClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def login_key(http: HTTPClient, username: str, api_key: str) -> str:
|
|
10
|
+
"""Authenticate with username + API key. Returns the session token."""
|
|
11
|
+
data = await http.post("/api/Auth/loginKey", {
|
|
12
|
+
"userName": username,
|
|
13
|
+
"apiKey": api_key,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
token = data.get("token")
|
|
17
|
+
if not token:
|
|
18
|
+
raise AuthenticationError("Login succeeded but no token returned")
|
|
19
|
+
|
|
20
|
+
return token
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def login_app(
|
|
24
|
+
http: HTTPClient,
|
|
25
|
+
username: str,
|
|
26
|
+
password: str,
|
|
27
|
+
device_id: str,
|
|
28
|
+
app_id: str,
|
|
29
|
+
verify_key: str,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Authenticate as an authorized application. Returns the session token."""
|
|
32
|
+
data = await http.post("/api/Auth/loginApp", {
|
|
33
|
+
"userName": username,
|
|
34
|
+
"password": password,
|
|
35
|
+
"deviceId": device_id,
|
|
36
|
+
"appId": app_id,
|
|
37
|
+
"verifyKey": verify_key,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
token = data.get("token")
|
|
41
|
+
if not token:
|
|
42
|
+
raise AuthenticationError("Login succeeded but no token returned")
|
|
43
|
+
|
|
44
|
+
return token
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def validate_token(http: HTTPClient) -> str | None:
|
|
48
|
+
"""Validate the current token and return a refreshed one if available."""
|
|
49
|
+
data = await http.post("/api/Auth/validate")
|
|
50
|
+
return data.get("newToken") or data.get("token")
|