lse-data 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.
- lse_data-0.1.0/.gitignore +4 -0
- lse_data-0.1.0/LICENSE +21 -0
- lse_data-0.1.0/PKG-INFO +178 -0
- lse_data-0.1.0/README.md +147 -0
- lse_data-0.1.0/examples/async_stream.py +20 -0
- lse_data-0.1.0/examples/basic_stream.py +14 -0
- lse_data-0.1.0/examples/callback_style.py +35 -0
- lse_data-0.1.0/examples/save_to_csv.py +23 -0
- lse_data-0.1.0/lse/__init__.py +21 -0
- lse_data-0.1.0/lse/client.py +360 -0
- lse_data-0.1.0/pyproject.toml +45 -0
lse_data-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 London Strategic Edge
|
|
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.
|
lse_data-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lse-data
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for London Strategic Edge real-time market data
|
|
5
|
+
Project-URL: Homepage, https://londonstrategicedge.com
|
|
6
|
+
Project-URL: Documentation, https://github.com/londonstrategicedge/lse-data
|
|
7
|
+
Project-URL: Repository, https://github.com/londonstrategicedge/lse-data
|
|
8
|
+
Project-URL: Issues, https://github.com/londonstrategicedge/lse-data/issues
|
|
9
|
+
Author-email: London Strategic Edge <support@londonstrategicedge.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: crypto,forex,market-data,options,real-time,stocks,trading,websocket
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Requires-Dist: websockets>=11.0
|
|
28
|
+
Provides-Extra: pandas
|
|
29
|
+
Requires-Dist: pandas>=1.5; extra == 'pandas'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# lse-data
|
|
33
|
+
|
|
34
|
+
Python SDK for [London Strategic Edge](https://londonstrategicedge.com) real-time market data.
|
|
35
|
+
|
|
36
|
+
Stream live prices for **2,000+ instruments** including stocks, crypto, forex, indices, commodities, ETFs, and options.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install lse-data
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from lse import LSE
|
|
48
|
+
|
|
49
|
+
client = LSE(api_key="your_api_key")
|
|
50
|
+
|
|
51
|
+
for tick in client.stream(["BTC/USD", "AAPL", "EUR/USD"]):
|
|
52
|
+
print(f"{tick.symbol}: ${tick.price}")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Get your API key at [londonstrategicedge.com/data](https://londonstrategicedge.com/data).
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- Real-time WebSocket streaming with auto-reconnect
|
|
60
|
+
- 2,000+ symbols: US/UK/EU/Asia stocks, crypto, forex, indices, commodities, options
|
|
61
|
+
- Sync and async interfaces
|
|
62
|
+
- Callback and iterator patterns
|
|
63
|
+
- Zero config, just an API key
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Stream ticks (simplest)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from lse import LSE
|
|
71
|
+
|
|
72
|
+
client = LSE(api_key="your_key")
|
|
73
|
+
|
|
74
|
+
for tick in client.stream(["BTC/USD", "ETH/USD", "AAPL"]):
|
|
75
|
+
print(f"{tick.symbol:12s} ${tick.price:>12,.2f}")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Callback style
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from lse import LSE
|
|
82
|
+
|
|
83
|
+
def on_tick(tick):
|
|
84
|
+
print(f"{tick.symbol}: {tick.price}")
|
|
85
|
+
|
|
86
|
+
client = LSE(api_key="your_key")
|
|
87
|
+
client.on("tick", on_tick)
|
|
88
|
+
client.connect(symbols=["BTC/USD", "ETH/USD"])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Async streaming
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import asyncio
|
|
95
|
+
from lse import LSE
|
|
96
|
+
|
|
97
|
+
async def main():
|
|
98
|
+
client = LSE(api_key="your_key")
|
|
99
|
+
async for tick in client.stream_async(["BTC/USD"]):
|
|
100
|
+
print(tick)
|
|
101
|
+
|
|
102
|
+
asyncio.run(main())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Save to CSV
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import csv, datetime
|
|
109
|
+
from lse import LSE
|
|
110
|
+
|
|
111
|
+
client = LSE(api_key="your_key")
|
|
112
|
+
|
|
113
|
+
with open("ticks.csv", "w", newline="") as f:
|
|
114
|
+
writer = csv.writer(f)
|
|
115
|
+
writer.writerow(["time", "symbol", "price", "bid", "ask"])
|
|
116
|
+
|
|
117
|
+
for tick in client.stream(["BTC/USD", "ETH/USD"]):
|
|
118
|
+
writer.writerow([
|
|
119
|
+
datetime.datetime.now().isoformat(),
|
|
120
|
+
tick.symbol, tick.price, tick.bid, tick.ask,
|
|
121
|
+
])
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Tick object
|
|
125
|
+
|
|
126
|
+
Each tick has these fields:
|
|
127
|
+
|
|
128
|
+
| Field | Type | Description |
|
|
129
|
+
|-------|------|-------------|
|
|
130
|
+
| `symbol` | `str` | Instrument symbol (e.g. `BTC/USD`, `AAPL`) |
|
|
131
|
+
| `price` | `float` | Latest price |
|
|
132
|
+
| `bid` | `float` | Bid price (if available) |
|
|
133
|
+
| `ask` | `float` | Ask price (if available) |
|
|
134
|
+
| `volume` | `float` | Volume (if available) |
|
|
135
|
+
| `timestamp` | `float` | Unix timestamp |
|
|
136
|
+
| `name` | `str` | Human-readable name (e.g. `Apple Inc.`) |
|
|
137
|
+
|
|
138
|
+
## Events
|
|
139
|
+
|
|
140
|
+
When using the callback style with `.on()`:
|
|
141
|
+
|
|
142
|
+
| Event | Callback args | Description |
|
|
143
|
+
|-------|---------------|-------------|
|
|
144
|
+
| `tick` | `Tick` | New price tick |
|
|
145
|
+
| `connected` | (none) | WebSocket connected |
|
|
146
|
+
| `authenticated` | (none) | API key accepted |
|
|
147
|
+
| `disconnected` | (none) | Connection lost (will auto-reconnect) |
|
|
148
|
+
| `error` | `str` | Error message |
|
|
149
|
+
|
|
150
|
+
## Available symbols
|
|
151
|
+
|
|
152
|
+
After connecting, `client.symbols` contains the full list of available instruments:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
client = LSE(api_key="your_key")
|
|
156
|
+
|
|
157
|
+
# stream() handles connection internally, but you can also connect manually:
|
|
158
|
+
import asyncio, json, websockets
|
|
159
|
+
|
|
160
|
+
async def list_symbols():
|
|
161
|
+
async with websockets.connect("wss://data-ws.londonstrategicedge.com") as ws:
|
|
162
|
+
await ws.recv() # welcome
|
|
163
|
+
await ws.send(json.dumps({"action": "auth", "api_key": "your_key"}))
|
|
164
|
+
data = json.loads(await ws.recv())
|
|
165
|
+
for s in data["symbols"]:
|
|
166
|
+
print(s["symbol"], s.get("name", ""))
|
|
167
|
+
|
|
168
|
+
asyncio.run(list_symbols())
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Requirements
|
|
172
|
+
|
|
173
|
+
- Python 3.8+
|
|
174
|
+
- `websockets` (installed automatically)
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
lse_data-0.1.0/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# lse-data
|
|
2
|
+
|
|
3
|
+
Python SDK for [London Strategic Edge](https://londonstrategicedge.com) real-time market data.
|
|
4
|
+
|
|
5
|
+
Stream live prices for **2,000+ instruments** including stocks, crypto, forex, indices, commodities, ETFs, and options.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install lse-data
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from lse import LSE
|
|
17
|
+
|
|
18
|
+
client = LSE(api_key="your_api_key")
|
|
19
|
+
|
|
20
|
+
for tick in client.stream(["BTC/USD", "AAPL", "EUR/USD"]):
|
|
21
|
+
print(f"{tick.symbol}: ${tick.price}")
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Get your API key at [londonstrategicedge.com/data](https://londonstrategicedge.com/data).
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- Real-time WebSocket streaming with auto-reconnect
|
|
29
|
+
- 2,000+ symbols: US/UK/EU/Asia stocks, crypto, forex, indices, commodities, options
|
|
30
|
+
- Sync and async interfaces
|
|
31
|
+
- Callback and iterator patterns
|
|
32
|
+
- Zero config, just an API key
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### Stream ticks (simplest)
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from lse import LSE
|
|
40
|
+
|
|
41
|
+
client = LSE(api_key="your_key")
|
|
42
|
+
|
|
43
|
+
for tick in client.stream(["BTC/USD", "ETH/USD", "AAPL"]):
|
|
44
|
+
print(f"{tick.symbol:12s} ${tick.price:>12,.2f}")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Callback style
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from lse import LSE
|
|
51
|
+
|
|
52
|
+
def on_tick(tick):
|
|
53
|
+
print(f"{tick.symbol}: {tick.price}")
|
|
54
|
+
|
|
55
|
+
client = LSE(api_key="your_key")
|
|
56
|
+
client.on("tick", on_tick)
|
|
57
|
+
client.connect(symbols=["BTC/USD", "ETH/USD"])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Async streaming
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import asyncio
|
|
64
|
+
from lse import LSE
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
client = LSE(api_key="your_key")
|
|
68
|
+
async for tick in client.stream_async(["BTC/USD"]):
|
|
69
|
+
print(tick)
|
|
70
|
+
|
|
71
|
+
asyncio.run(main())
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Save to CSV
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import csv, datetime
|
|
78
|
+
from lse import LSE
|
|
79
|
+
|
|
80
|
+
client = LSE(api_key="your_key")
|
|
81
|
+
|
|
82
|
+
with open("ticks.csv", "w", newline="") as f:
|
|
83
|
+
writer = csv.writer(f)
|
|
84
|
+
writer.writerow(["time", "symbol", "price", "bid", "ask"])
|
|
85
|
+
|
|
86
|
+
for tick in client.stream(["BTC/USD", "ETH/USD"]):
|
|
87
|
+
writer.writerow([
|
|
88
|
+
datetime.datetime.now().isoformat(),
|
|
89
|
+
tick.symbol, tick.price, tick.bid, tick.ask,
|
|
90
|
+
])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Tick object
|
|
94
|
+
|
|
95
|
+
Each tick has these fields:
|
|
96
|
+
|
|
97
|
+
| Field | Type | Description |
|
|
98
|
+
|-------|------|-------------|
|
|
99
|
+
| `symbol` | `str` | Instrument symbol (e.g. `BTC/USD`, `AAPL`) |
|
|
100
|
+
| `price` | `float` | Latest price |
|
|
101
|
+
| `bid` | `float` | Bid price (if available) |
|
|
102
|
+
| `ask` | `float` | Ask price (if available) |
|
|
103
|
+
| `volume` | `float` | Volume (if available) |
|
|
104
|
+
| `timestamp` | `float` | Unix timestamp |
|
|
105
|
+
| `name` | `str` | Human-readable name (e.g. `Apple Inc.`) |
|
|
106
|
+
|
|
107
|
+
## Events
|
|
108
|
+
|
|
109
|
+
When using the callback style with `.on()`:
|
|
110
|
+
|
|
111
|
+
| Event | Callback args | Description |
|
|
112
|
+
|-------|---------------|-------------|
|
|
113
|
+
| `tick` | `Tick` | New price tick |
|
|
114
|
+
| `connected` | (none) | WebSocket connected |
|
|
115
|
+
| `authenticated` | (none) | API key accepted |
|
|
116
|
+
| `disconnected` | (none) | Connection lost (will auto-reconnect) |
|
|
117
|
+
| `error` | `str` | Error message |
|
|
118
|
+
|
|
119
|
+
## Available symbols
|
|
120
|
+
|
|
121
|
+
After connecting, `client.symbols` contains the full list of available instruments:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
client = LSE(api_key="your_key")
|
|
125
|
+
|
|
126
|
+
# stream() handles connection internally, but you can also connect manually:
|
|
127
|
+
import asyncio, json, websockets
|
|
128
|
+
|
|
129
|
+
async def list_symbols():
|
|
130
|
+
async with websockets.connect("wss://data-ws.londonstrategicedge.com") as ws:
|
|
131
|
+
await ws.recv() # welcome
|
|
132
|
+
await ws.send(json.dumps({"action": "auth", "api_key": "your_key"}))
|
|
133
|
+
data = json.loads(await ws.recv())
|
|
134
|
+
for s in data["symbols"]:
|
|
135
|
+
print(s["symbol"], s.get("name", ""))
|
|
136
|
+
|
|
137
|
+
asyncio.run(list_symbols())
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Requirements
|
|
141
|
+
|
|
142
|
+
- Python 3.8+
|
|
143
|
+
- `websockets` (installed automatically)
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async streaming example - for use in async applications.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
pip install lse-data
|
|
6
|
+
python async_stream.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from lse import LSE
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def main():
|
|
14
|
+
client = LSE(api_key="YOUR_API_KEY")
|
|
15
|
+
|
|
16
|
+
async for tick in client.stream_async(["BTC/USD", "AAPL", "EUR/USD"]):
|
|
17
|
+
print(f"{tick.symbol:12s} {tick.price:>12,.2f} {tick.name or ''}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic streaming example - print live prices to the terminal.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
pip install lse-data
|
|
6
|
+
python basic_stream.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from lse import LSE
|
|
10
|
+
|
|
11
|
+
client = LSE(api_key="YOUR_API_KEY")
|
|
12
|
+
|
|
13
|
+
for tick in client.stream(["BTC/USD", "ETH/USD", "AAPL", "EUR/USD"]):
|
|
14
|
+
print(f"{tick.symbol:12s} ${tick.price:>12,.2f}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Callback-style example - event-driven tick handling.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
pip install lse-data
|
|
6
|
+
python callback_style.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from lse import LSE
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def on_tick(tick):
|
|
13
|
+
print(f"[TICK] {tick.symbol}: {tick.price}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def on_connected():
|
|
17
|
+
print("Connected to LSE")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def on_authenticated():
|
|
21
|
+
print("Authenticated successfully")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def on_error(msg):
|
|
25
|
+
print(f"[ERROR] {msg}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
client = LSE(api_key="YOUR_API_KEY")
|
|
29
|
+
client.on("tick", on_tick)
|
|
30
|
+
client.on("connected", on_connected)
|
|
31
|
+
client.on("authenticated", on_authenticated)
|
|
32
|
+
client.on("error", on_error)
|
|
33
|
+
|
|
34
|
+
# connect() blocks forever, ticks arrive via on_tick callback
|
|
35
|
+
client.connect(symbols=["BTC/USD", "ETH/USD", "SOL/USD"])
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Save live ticks to a CSV file.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
pip install lse-data
|
|
6
|
+
python save_to_csv.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import csv
|
|
10
|
+
import datetime
|
|
11
|
+
from lse import LSE
|
|
12
|
+
|
|
13
|
+
client = LSE(api_key="YOUR_API_KEY")
|
|
14
|
+
|
|
15
|
+
with open("ticks.csv", "w", newline="") as f:
|
|
16
|
+
writer = csv.writer(f)
|
|
17
|
+
writer.writerow(["timestamp", "symbol", "price", "bid", "ask"])
|
|
18
|
+
|
|
19
|
+
for tick in client.stream(["BTC/USD", "ETH/USD"]):
|
|
20
|
+
now = datetime.datetime.now().isoformat()
|
|
21
|
+
writer.writerow([now, tick.symbol, tick.price, tick.bid, tick.ask])
|
|
22
|
+
f.flush() # write immediately so you can tail the file
|
|
23
|
+
print(f"{now} {tick.symbol} {tick.price}")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
London Strategic Edge - Python SDK for real-time market data.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from lse import LSE
|
|
6
|
+
|
|
7
|
+
client = LSE(api_key="your_api_key")
|
|
8
|
+
|
|
9
|
+
# Stream live ticks
|
|
10
|
+
for tick in client.stream(["BTC/USD", "AAPL", "EUR/USD"]):
|
|
11
|
+
print(f"{tick.symbol} {tick.price}")
|
|
12
|
+
|
|
13
|
+
# Async streaming
|
|
14
|
+
async for tick in client.stream_async(["BTC/USD"]):
|
|
15
|
+
print(tick)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from lse.client import LSE, Tick
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
__all__ = ["LSE", "Tick"]
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LSE WebSocket client for real-time market data streaming.
|
|
3
|
+
|
|
4
|
+
Connects to wss://data-ws.londonstrategicedge.com and provides a clean
|
|
5
|
+
interface for subscribing to symbols and receiving live price ticks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Callable, Dict, Iterator, List, Optional, Set
|
|
14
|
+
|
|
15
|
+
import websockets
|
|
16
|
+
import websockets.client
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
WS_URL = "wss://data-ws.londonstrategicedge.com"
|
|
20
|
+
|
|
21
|
+
# Ping interval to keep the connection alive. The server expects a ping
|
|
22
|
+
# within its idle timeout window (currently 600s). We send every 25s to
|
|
23
|
+
# stay well under the server's 30s protocol-level ping interval.
|
|
24
|
+
PING_INTERVAL = 25
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Tick:
|
|
29
|
+
"""A single price tick from the LSE feed."""
|
|
30
|
+
symbol: str
|
|
31
|
+
price: float
|
|
32
|
+
bid: Optional[float] = None
|
|
33
|
+
ask: Optional[float] = None
|
|
34
|
+
volume: Optional[float] = None
|
|
35
|
+
timestamp: Optional[float] = None
|
|
36
|
+
name: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return f"Tick({self.symbol} {self.price})"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LSE:
|
|
43
|
+
"""Client for London Strategic Edge real-time market data.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
api_key: Your LSE API key. Get one at https://londonstrategicedge.com/data
|
|
47
|
+
url: WebSocket endpoint. Defaults to the production server.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
Synchronous streaming (simplest):
|
|
51
|
+
|
|
52
|
+
from lse import LSE
|
|
53
|
+
|
|
54
|
+
client = LSE(api_key="your_key")
|
|
55
|
+
for tick in client.stream(["BTC/USD", "AAPL"]):
|
|
56
|
+
print(tick.symbol, tick.price)
|
|
57
|
+
|
|
58
|
+
With a callback:
|
|
59
|
+
|
|
60
|
+
def on_tick(tick):
|
|
61
|
+
print(f"{tick.symbol}: {tick.price}")
|
|
62
|
+
|
|
63
|
+
client = LSE(api_key="your_key")
|
|
64
|
+
client.on("tick", on_tick)
|
|
65
|
+
client.subscribe(["BTC/USD", "ETH/USD"])
|
|
66
|
+
client.connect() # blocks forever
|
|
67
|
+
|
|
68
|
+
Async streaming:
|
|
69
|
+
|
|
70
|
+
import asyncio
|
|
71
|
+
from lse import LSE
|
|
72
|
+
|
|
73
|
+
async def main():
|
|
74
|
+
client = LSE(api_key="your_key")
|
|
75
|
+
async for tick in client.stream_async(["BTC/USD"]):
|
|
76
|
+
print(tick)
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, api_key: str, url: str = WS_URL):
|
|
82
|
+
self._api_key = api_key
|
|
83
|
+
self._url = url
|
|
84
|
+
self._ws: Optional[websockets.client.WebSocketClientProtocol] = None
|
|
85
|
+
self._callbacks: Dict[str, List[Callable]] = {}
|
|
86
|
+
self._symbols: List[dict] = []
|
|
87
|
+
self._tier: str = ""
|
|
88
|
+
self._authenticated = False
|
|
89
|
+
self._subscriptions: Set[str] = set()
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
# Public properties
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def symbols(self) -> List[dict]:
|
|
97
|
+
"""List of available symbols returned after authentication."""
|
|
98
|
+
return self._symbols
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def tier(self) -> str:
|
|
102
|
+
"""Account tier (e.g. 'basic', 'pro')."""
|
|
103
|
+
return self._tier
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def authenticated(self) -> bool:
|
|
107
|
+
"""Whether the client has successfully authenticated."""
|
|
108
|
+
return self._authenticated
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def subscriptions(self) -> Set[str]:
|
|
112
|
+
"""Set of currently subscribed symbols."""
|
|
113
|
+
return self._subscriptions.copy()
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
# Event callbacks
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def on(self, event: str, callback: Callable) -> "LSE":
|
|
120
|
+
"""Register a callback for an event.
|
|
121
|
+
|
|
122
|
+
Supported events:
|
|
123
|
+
- "tick": called with a Tick object on each price update
|
|
124
|
+
- "connected": called when WebSocket connects
|
|
125
|
+
- "authenticated": called when auth succeeds
|
|
126
|
+
- "disconnected": called when connection drops
|
|
127
|
+
- "error": called with error message string
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
event: Event name.
|
|
131
|
+
callback: Function to call.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
self, for chaining.
|
|
135
|
+
"""
|
|
136
|
+
self._callbacks.setdefault(event, []).append(callback)
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def _emit(self, event: str, *args):
|
|
140
|
+
for cb in self._callbacks.get(event, []):
|
|
141
|
+
try:
|
|
142
|
+
cb(*args)
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
# Synchronous API (blocking)
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def stream(self, symbols: List[str], reconnect: bool = True) -> Iterator[Tick]:
|
|
151
|
+
"""Stream live ticks. Blocks forever, yields Tick objects.
|
|
152
|
+
|
|
153
|
+
This is the simplest way to consume live data. It handles connection,
|
|
154
|
+
authentication, subscription, ping/pong, and auto-reconnect internally.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
symbols: List of symbols to subscribe to (e.g. ["BTC/USD", "AAPL"]).
|
|
158
|
+
reconnect: If True, automatically reconnect on disconnect.
|
|
159
|
+
|
|
160
|
+
Yields:
|
|
161
|
+
Tick objects as they arrive.
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
from lse import LSE
|
|
165
|
+
|
|
166
|
+
client = LSE(api_key="your_key")
|
|
167
|
+
for tick in client.stream(["BTC/USD", "ETH/USD", "AAPL"]):
|
|
168
|
+
print(f"{tick.symbol}: ${tick.price}")
|
|
169
|
+
"""
|
|
170
|
+
while True:
|
|
171
|
+
try:
|
|
172
|
+
# Run the async generator in a new event loop.
|
|
173
|
+
# We suppress "task was destroyed" warnings that occur when
|
|
174
|
+
# the caller breaks out of the iterator mid-stream, which is
|
|
175
|
+
# normal usage (e.g. "for tick in stream: if done: break").
|
|
176
|
+
import warnings
|
|
177
|
+
loop = asyncio.new_event_loop()
|
|
178
|
+
try:
|
|
179
|
+
gen = self.stream_async(symbols, reconnect=False).__aiter__()
|
|
180
|
+
while True:
|
|
181
|
+
tick = loop.run_until_complete(gen.__anext__())
|
|
182
|
+
yield tick
|
|
183
|
+
except StopAsyncIteration:
|
|
184
|
+
pass
|
|
185
|
+
except GeneratorExit:
|
|
186
|
+
return
|
|
187
|
+
finally:
|
|
188
|
+
# Shut down pending tasks cleanly to avoid warnings
|
|
189
|
+
pending = asyncio.all_tasks(loop)
|
|
190
|
+
for task in pending:
|
|
191
|
+
task.cancel()
|
|
192
|
+
if pending:
|
|
193
|
+
with warnings.catch_warnings():
|
|
194
|
+
warnings.simplefilter("ignore")
|
|
195
|
+
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
|
196
|
+
loop.close()
|
|
197
|
+
except Exception as e:
|
|
198
|
+
self._emit("error", str(e))
|
|
199
|
+
if not reconnect:
|
|
200
|
+
raise
|
|
201
|
+
time.sleep(3)
|
|
202
|
+
|
|
203
|
+
def connect(self, symbols: Optional[List[str]] = None):
|
|
204
|
+
"""Connect and block forever, dispatching events via callbacks.
|
|
205
|
+
|
|
206
|
+
Use this with .on("tick", callback) for event-driven usage.
|
|
207
|
+
For iterator-style usage, use .stream() instead.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
symbols: Optional list of symbols to subscribe to on connect.
|
|
211
|
+
You can also call .subscribe() separately.
|
|
212
|
+
"""
|
|
213
|
+
asyncio.run(self._run_forever(symbols or []))
|
|
214
|
+
|
|
215
|
+
def subscribe(self, symbols: List[str]):
|
|
216
|
+
"""Subscribe to additional symbols (only works during .connect()).
|
|
217
|
+
|
|
218
|
+
For most use cases, pass symbols directly to .stream() or .connect().
|
|
219
|
+
"""
|
|
220
|
+
for sym in symbols:
|
|
221
|
+
self._subscriptions.add(sym)
|
|
222
|
+
if self._ws:
|
|
223
|
+
asyncio.get_event_loop().run_until_complete(
|
|
224
|
+
self._ws.send(json.dumps({"action": "subscribe", "symbol": sym}))
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
# Async API
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
async def stream_async(self, symbols: List[str], reconnect: bool = True):
|
|
232
|
+
"""Async generator that yields Tick objects.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
symbols: List of symbols to subscribe to.
|
|
236
|
+
reconnect: If True, automatically reconnect on disconnect.
|
|
237
|
+
|
|
238
|
+
Yields:
|
|
239
|
+
Tick objects as they arrive.
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
import asyncio
|
|
243
|
+
from lse import LSE
|
|
244
|
+
|
|
245
|
+
async def main():
|
|
246
|
+
client = LSE(api_key="your_key")
|
|
247
|
+
async for tick in client.stream_async(["BTC/USD"]):
|
|
248
|
+
print(tick)
|
|
249
|
+
|
|
250
|
+
asyncio.run(main())
|
|
251
|
+
"""
|
|
252
|
+
while True:
|
|
253
|
+
try:
|
|
254
|
+
async for tick in self._stream_once(symbols):
|
|
255
|
+
yield tick
|
|
256
|
+
except Exception as e:
|
|
257
|
+
self._emit("error", str(e))
|
|
258
|
+
self._emit("disconnected")
|
|
259
|
+
if not reconnect:
|
|
260
|
+
return
|
|
261
|
+
await asyncio.sleep(3)
|
|
262
|
+
|
|
263
|
+
async def connect_async(self, symbols: Optional[List[str]] = None):
|
|
264
|
+
"""Async version of connect(). Blocks forever."""
|
|
265
|
+
await self._run_forever(symbols or [])
|
|
266
|
+
|
|
267
|
+
# ------------------------------------------------------------------
|
|
268
|
+
# Internal
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
async def _stream_once(self, symbols: List[str]):
|
|
272
|
+
"""Single connection session. Yields ticks until disconnect."""
|
|
273
|
+
async with websockets.connect(
|
|
274
|
+
self._url,
|
|
275
|
+
ping_interval=PING_INTERVAL,
|
|
276
|
+
ping_timeout=30,
|
|
277
|
+
) as ws:
|
|
278
|
+
self._ws = ws
|
|
279
|
+
self._authenticated = False
|
|
280
|
+
|
|
281
|
+
# Wait for welcome
|
|
282
|
+
raw = await ws.recv()
|
|
283
|
+
msg = json.loads(raw)
|
|
284
|
+
if msg.get("type") == "welcome":
|
|
285
|
+
self._emit("connected")
|
|
286
|
+
|
|
287
|
+
# Authenticate
|
|
288
|
+
await ws.send(json.dumps({
|
|
289
|
+
"action": "auth",
|
|
290
|
+
"api_key": self._api_key,
|
|
291
|
+
}))
|
|
292
|
+
|
|
293
|
+
# Start keepalive pings in background
|
|
294
|
+
ping_task = asyncio.create_task(self._ping_loop(ws))
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
async for raw in ws:
|
|
298
|
+
msg = json.loads(raw)
|
|
299
|
+
msg_type = msg.get("type")
|
|
300
|
+
|
|
301
|
+
if msg_type == "authenticated":
|
|
302
|
+
self._authenticated = True
|
|
303
|
+
self._tier = msg.get("tier", "")
|
|
304
|
+
self._symbols = msg.get("symbols", [])
|
|
305
|
+
self._emit("authenticated")
|
|
306
|
+
|
|
307
|
+
# Subscribe to requested symbols
|
|
308
|
+
for sym in symbols:
|
|
309
|
+
self._subscriptions.add(sym)
|
|
310
|
+
await ws.send(json.dumps({
|
|
311
|
+
"action": "subscribe",
|
|
312
|
+
"symbol": sym,
|
|
313
|
+
}))
|
|
314
|
+
|
|
315
|
+
elif msg_type == "tick":
|
|
316
|
+
tick = Tick(
|
|
317
|
+
symbol=msg.get("symbol", ""),
|
|
318
|
+
price=msg.get("price", 0.0),
|
|
319
|
+
bid=msg.get("bid"),
|
|
320
|
+
ask=msg.get("ask"),
|
|
321
|
+
volume=msg.get("volume"),
|
|
322
|
+
timestamp=msg.get("ts"),
|
|
323
|
+
name=msg.get("name"),
|
|
324
|
+
)
|
|
325
|
+
self._emit("tick", tick)
|
|
326
|
+
yield tick
|
|
327
|
+
|
|
328
|
+
elif msg_type == "error":
|
|
329
|
+
self._emit("error", msg.get("message", "Unknown error"))
|
|
330
|
+
|
|
331
|
+
elif msg_type == "pong":
|
|
332
|
+
pass # keepalive response, ignore
|
|
333
|
+
|
|
334
|
+
finally:
|
|
335
|
+
ping_task.cancel()
|
|
336
|
+
self._ws = None
|
|
337
|
+
self._authenticated = False
|
|
338
|
+
|
|
339
|
+
async def _run_forever(self, symbols: List[str]):
|
|
340
|
+
"""Connect, subscribe, and dispatch events forever with auto-reconnect."""
|
|
341
|
+
while True:
|
|
342
|
+
try:
|
|
343
|
+
async for tick in self._stream_once(symbols):
|
|
344
|
+
pass # ticks dispatched via callbacks in _stream_once
|
|
345
|
+
except Exception as e:
|
|
346
|
+
self._emit("error", str(e))
|
|
347
|
+
self._emit("disconnected")
|
|
348
|
+
await asyncio.sleep(3)
|
|
349
|
+
|
|
350
|
+
async def _ping_loop(self, ws):
|
|
351
|
+
"""Send application-level pings to keep the connection alive."""
|
|
352
|
+
try:
|
|
353
|
+
while True:
|
|
354
|
+
await asyncio.sleep(PING_INTERVAL)
|
|
355
|
+
try:
|
|
356
|
+
await ws.send(json.dumps({"action": "ping"}))
|
|
357
|
+
except Exception:
|
|
358
|
+
break
|
|
359
|
+
except asyncio.CancelledError:
|
|
360
|
+
pass
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lse-data"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for London Strategic Edge real-time market data"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "London Strategic Edge", email = "support@londonstrategicedge.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["trading", "market-data", "websocket", "stocks", "crypto", "forex", "options", "real-time"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"websockets>=11.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
pandas = ["pandas>=1.5"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["lse"]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://londonstrategicedge.com"
|
|
43
|
+
Documentation = "https://github.com/londonstrategicedge/lse-data"
|
|
44
|
+
Repository = "https://github.com/londonstrategicedge/lse-data"
|
|
45
|
+
Issues = "https://github.com/londonstrategicedge/lse-data/issues"
|