tickstream 0.1.0__py3-none-any.whl
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.
tickstream/__init__.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""tickstream — realtime CME futures, options & Level 2. One key, one line.
|
|
2
|
+
|
|
3
|
+
from tickstream import Stream
|
|
4
|
+
for tick in Stream("sk_live_…").subscribe("ES"):
|
|
5
|
+
print(tick.price, tick.size, tick.ts)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import urllib.parse
|
|
11
|
+
import urllib.request
|
|
12
|
+
|
|
13
|
+
import websocket # websocket-client
|
|
14
|
+
|
|
15
|
+
__all__ = ["Stream", "Client"]
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
DEFAULT_WS = "wss://stream.tick-stream.xyz/v1"
|
|
19
|
+
DEFAULT_API = "https://api.tick-stream.xyz/v1"
|
|
20
|
+
_CH_TYPE = {"ticks": "tick", "book": "book", "options": "option"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Obj(dict):
|
|
24
|
+
"""A dict that also allows attribute access: msg.price as well as msg['price']."""
|
|
25
|
+
|
|
26
|
+
def __getattr__(self, name):
|
|
27
|
+
try:
|
|
28
|
+
return self[name]
|
|
29
|
+
except KeyError:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Stream:
|
|
34
|
+
"""Live market-data stream. Iterate the result of subscribe()/book()/options().
|
|
35
|
+
|
|
36
|
+
Blocking, synchronous iteration — reconnects automatically and replies to pings.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, api_key, url=None):
|
|
40
|
+
if not api_key:
|
|
41
|
+
raise ValueError("tickstream: an API key is required")
|
|
42
|
+
self.api_key = api_key
|
|
43
|
+
self.url = url or os.environ.get("TICKSTREAM_WS_URL", DEFAULT_WS)
|
|
44
|
+
self.plan = None
|
|
45
|
+
|
|
46
|
+
def subscribe(self, *symbols):
|
|
47
|
+
"""Yield live trade ticks for one or more symbols."""
|
|
48
|
+
return self._run("ticks", symbols)
|
|
49
|
+
|
|
50
|
+
def book(self, *symbols):
|
|
51
|
+
"""Yield Level 2 order-book snapshots (Realtime+L2 / Pro plans)."""
|
|
52
|
+
return self._run("book", symbols)
|
|
53
|
+
|
|
54
|
+
def options(self, *symbols):
|
|
55
|
+
"""Yield option quotes with greeks for one or more underlyings (Pro plan)."""
|
|
56
|
+
return self._run("options", symbols)
|
|
57
|
+
|
|
58
|
+
def _run(self, channel, symbols):
|
|
59
|
+
want = _CH_TYPE[channel]
|
|
60
|
+
syms = [str(s).upper() for s in symbols]
|
|
61
|
+
sep = "&" if "?" in self.url else "?"
|
|
62
|
+
full = "%s%skey=%s" % (self.url, sep, urllib.parse.quote(self.api_key))
|
|
63
|
+
while True:
|
|
64
|
+
ws = websocket.create_connection(full, enable_multithread=True)
|
|
65
|
+
try:
|
|
66
|
+
ws.send(json.dumps({"op": "subscribe", "channel": channel, "symbols": syms}))
|
|
67
|
+
while True:
|
|
68
|
+
raw = ws.recv()
|
|
69
|
+
if not raw:
|
|
70
|
+
break
|
|
71
|
+
msg = json.loads(raw)
|
|
72
|
+
t = msg.get("type")
|
|
73
|
+
if t == "ping":
|
|
74
|
+
ws.send(json.dumps({"op": "pong"}))
|
|
75
|
+
continue
|
|
76
|
+
if t == "welcome":
|
|
77
|
+
self.plan = msg.get("plan")
|
|
78
|
+
continue
|
|
79
|
+
if t == "error":
|
|
80
|
+
raise RuntimeError(msg.get("error", {}).get("message", "stream error"))
|
|
81
|
+
if t == want:
|
|
82
|
+
yield _Obj(msg)
|
|
83
|
+
finally:
|
|
84
|
+
try:
|
|
85
|
+
ws.close()
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
# connection dropped — reconnect and resubscribe
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Client:
|
|
92
|
+
"""REST client for latest quotes, reference data and historical backfill."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, api_key, base=None):
|
|
95
|
+
if not api_key:
|
|
96
|
+
raise ValueError("tickstream: an API key is required")
|
|
97
|
+
self.api_key = api_key
|
|
98
|
+
self.base = (base or os.environ.get("TICKSTREAM_API_URL", DEFAULT_API)).rstrip("/")
|
|
99
|
+
|
|
100
|
+
def _get(self, path):
|
|
101
|
+
req = urllib.request.Request(
|
|
102
|
+
self.base + path, headers={"Authorization": "Bearer " + self.api_key}
|
|
103
|
+
)
|
|
104
|
+
with urllib.request.urlopen(req) as resp:
|
|
105
|
+
return json.loads(resp.read().decode())
|
|
106
|
+
|
|
107
|
+
def quote(self, symbol):
|
|
108
|
+
return self._get("/quote?symbol=" + urllib.parse.quote(symbol.upper()))
|
|
109
|
+
|
|
110
|
+
def ticks(self, symbol, start=None, end=None, cursor=None, limit=None):
|
|
111
|
+
q = {"symbol": symbol.upper()}
|
|
112
|
+
if start:
|
|
113
|
+
q["start"] = start
|
|
114
|
+
if end:
|
|
115
|
+
q["end"] = end
|
|
116
|
+
if cursor:
|
|
117
|
+
q["cursor"] = cursor
|
|
118
|
+
if limit:
|
|
119
|
+
q["limit"] = limit
|
|
120
|
+
return self._get("/ticks?" + urllib.parse.urlencode(q))
|
|
121
|
+
|
|
122
|
+
def symbols(self):
|
|
123
|
+
return self._get("/symbols")
|
|
124
|
+
|
|
125
|
+
def options(self, underlying, expiry=None):
|
|
126
|
+
q = {"underlying": underlying.upper()}
|
|
127
|
+
if expiry:
|
|
128
|
+
q["expiry"] = expiry
|
|
129
|
+
return self._get("/options?" + urllib.parse.urlencode(q))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tickstream
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Realtime CME futures, options & Level 2 market data — one key, one line.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://tick-stream.xyz
|
|
7
|
+
Project-URL: Documentation, https://tick-stream.xyz/docs
|
|
8
|
+
Keywords: market-data,futures,options,cme,websocket,trading,ticks,level2
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: websocket-client>=1.7
|
|
12
|
+
|
|
13
|
+
# tickstream (Python)
|
|
14
|
+
|
|
15
|
+
Realtime CME futures, options & Level 2 — one key, one line.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install tickstream
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Stream ticks
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from tickstream import Stream
|
|
25
|
+
|
|
26
|
+
for tick in Stream("sk_live_…").subscribe("ES", "NQ"):
|
|
27
|
+
print(tick.symbol, tick.price, tick.size, tick.side)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Level 2 & options (Pro plans)
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from tickstream import Stream
|
|
34
|
+
|
|
35
|
+
for book in Stream("sk_live_…").book("ES"):
|
|
36
|
+
print(book.bids[0], book.asks[0], book.imbalance)
|
|
37
|
+
|
|
38
|
+
for o in Stream("sk_live_…").options("ES", "SPX"):
|
|
39
|
+
print(o.symbol, o.strike, o.right, o.iv, o.delta)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## REST: quotes, backfill, reference data
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from tickstream import Client
|
|
46
|
+
|
|
47
|
+
api = Client("sk_live_…")
|
|
48
|
+
api.quote("ES")
|
|
49
|
+
api.ticks("ES", start="2025-06-01T13:30:00Z", end="2025-06-01T20:00:00Z")
|
|
50
|
+
api.symbols()
|
|
51
|
+
api.options("SPX", expiry="2026-06-19")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Each message supports attribute **and** dict access (`tick.price` or `tick["price"]`).
|
|
55
|
+
|
|
56
|
+
Get your API key at <https://tick-stream.xyz>. Full docs: <https://tick-stream.xyz/docs>.
|
|
57
|
+
Point at a local gateway with `TICKSTREAM_WS_URL` / `TICKSTREAM_API_URL`.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
tickstream/__init__.py,sha256=HtaFB3W2deXztEc4kXYhpXZ2BO24X6tOUZm4BNm-TE8,4375
|
|
2
|
+
tickstream-0.1.0.dist-info/METADATA,sha256=784onNRMARf2CISuME393yg8-E8T4b7WfzCg9zY7U90,1530
|
|
3
|
+
tickstream-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
tickstream-0.1.0.dist-info/top_level.txt,sha256=my280qeUame35zcmt61Rh2F3AVTDdq2_7DzQiDXRASA,11
|
|
5
|
+
tickstream-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tickstream
|