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.
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ *.egg-info/
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.
@@ -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
@@ -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"