polyws 0.2.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.
- polyws/__init__.py +35 -0
- polyws/cli.py +266 -0
- polyws/gamma.py +60 -0
- polyws/logger.py +5 -0
- polyws/manager.py +471 -0
- polyws/order_book.py +172 -0
- polyws/types.py +182 -0
- polyws-0.2.0.dist-info/METADATA +294 -0
- polyws-0.2.0.dist-info/RECORD +12 -0
- polyws-0.2.0.dist-info/WHEEL +4 -0
- polyws-0.2.0.dist-info/entry_points.txt +2 -0
- polyws-0.2.0.dist-info/licenses/LICENSE +21 -0
polyws/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""polyws — Real-time Polymarket CLOB WebSocket streaming for Python."""
|
|
2
|
+
|
|
3
|
+
from polyws.gamma import extract_asset_ids, fetch_markets
|
|
4
|
+
from polyws.manager import ManagerStatistics, WSSubscriptionManager
|
|
5
|
+
from polyws.types import (
|
|
6
|
+
Book,
|
|
7
|
+
BookEvent,
|
|
8
|
+
ConnectionStatus,
|
|
9
|
+
LastTradePriceEvent,
|
|
10
|
+
PolymarketPriceUpdateEvent,
|
|
11
|
+
PolymarketWSEvent,
|
|
12
|
+
PriceChangeEvent,
|
|
13
|
+
PriceChangeItem,
|
|
14
|
+
PriceLevel,
|
|
15
|
+
TickSizeChangeEvent,
|
|
16
|
+
WebSocketHandlers,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"fetch_markets",
|
|
21
|
+
"extract_asset_ids",
|
|
22
|
+
"WSSubscriptionManager",
|
|
23
|
+
"ManagerStatistics",
|
|
24
|
+
"WebSocketHandlers",
|
|
25
|
+
"BookEvent",
|
|
26
|
+
"PriceChangeEvent",
|
|
27
|
+
"PriceChangeItem",
|
|
28
|
+
"LastTradePriceEvent",
|
|
29
|
+
"TickSizeChangeEvent",
|
|
30
|
+
"PolymarketPriceUpdateEvent",
|
|
31
|
+
"PriceLevel",
|
|
32
|
+
"Book",
|
|
33
|
+
"ConnectionStatus",
|
|
34
|
+
"PolymarketWSEvent",
|
|
35
|
+
]
|
polyws/cli.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""polyws CLI — stream Polymarket data from your terminal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import orjson
|
|
12
|
+
|
|
13
|
+
from polyws.gamma import extract_asset_ids, fetch_markets
|
|
14
|
+
from polyws.manager import WSSubscriptionManager
|
|
15
|
+
from polyws.types import (
|
|
16
|
+
BookEvent,
|
|
17
|
+
LastTradePriceEvent,
|
|
18
|
+
PolymarketPriceUpdateEvent,
|
|
19
|
+
PriceChangeEvent,
|
|
20
|
+
WebSocketHandlers,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
IS_TTY: bool = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── Output helpers ───────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _json_line(data: dict) -> None:
|
|
30
|
+
"""Write a JSON line to stdout and flush."""
|
|
31
|
+
sys.stdout.buffer.write(orjson.dumps(data) + b"\n")
|
|
32
|
+
sys.stdout.buffer.flush()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _pretty(text: str) -> None:
|
|
36
|
+
"""Write a pretty line to stdout."""
|
|
37
|
+
sys.stdout.write(text + "\n")
|
|
38
|
+
sys.stdout.flush()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _format_volume(vol: float) -> str:
|
|
42
|
+
"""Format volume as human-readable string."""
|
|
43
|
+
if vol >= 1_000_000:
|
|
44
|
+
return f"${vol / 1_000_000:.1f}M"
|
|
45
|
+
if vol >= 1_000:
|
|
46
|
+
return f"${vol / 1_000:.0f}K"
|
|
47
|
+
return f"${vol:.0f}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── Markets command ──────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _cmd_markets(args: argparse.Namespace) -> None:
|
|
54
|
+
"""Fetch and display markets from Gamma API."""
|
|
55
|
+
markets = fetch_markets(
|
|
56
|
+
limit=args.limit,
|
|
57
|
+
sort=args.sort,
|
|
58
|
+
search=args.search,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for m in markets:
|
|
62
|
+
question = m.get("question", "Unknown")
|
|
63
|
+
volume = float(m.get("volumeNum", 0) or 0)
|
|
64
|
+
# outcomePrices is a JSON-encoded list like '["0.65","0.35"]'
|
|
65
|
+
try:
|
|
66
|
+
prices = json.loads(m.get("outcomePrices", "[]"))
|
|
67
|
+
price = float(prices[0]) if prices else 0.0
|
|
68
|
+
except (json.JSONDecodeError, IndexError, ValueError):
|
|
69
|
+
price = 0.0
|
|
70
|
+
|
|
71
|
+
# Extract first asset_id
|
|
72
|
+
try:
|
|
73
|
+
token_ids = json.loads(m.get("clobTokenIds", "[]"))
|
|
74
|
+
asset_id = token_ids[0] if token_ids else ""
|
|
75
|
+
except (json.JSONDecodeError, IndexError):
|
|
76
|
+
asset_id = ""
|
|
77
|
+
|
|
78
|
+
slug = m.get("slug", "")
|
|
79
|
+
|
|
80
|
+
if IS_TTY:
|
|
81
|
+
trunc_q = question[:50] + ("..." if len(question) > 50 else "")
|
|
82
|
+
trunc_id = asset_id[:8] + "..." if len(asset_id) > 8 else asset_id
|
|
83
|
+
_pretty(
|
|
84
|
+
f" {price * 100:5.1f}% {_format_volume(volume):>7s} {trunc_q:<54s} [{trunc_id}]"
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
_json_line({
|
|
88
|
+
"asset_id": asset_id,
|
|
89
|
+
"question": question,
|
|
90
|
+
"volume": volume,
|
|
91
|
+
"price": f"{price:.4f}",
|
|
92
|
+
"market_slug": slug,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ── Stream command ───────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def _cmd_stream(args: argparse.Namespace) -> None:
|
|
100
|
+
"""Stream real-time events from Polymarket CLOB WebSocket."""
|
|
101
|
+
# Resolve asset IDs
|
|
102
|
+
asset_map: dict[str, str] = {} # asset_id -> question
|
|
103
|
+
|
|
104
|
+
if args.asset_id:
|
|
105
|
+
for aid in args.asset_id:
|
|
106
|
+
asset_map[aid] = aid[:12] + "..."
|
|
107
|
+
elif args.search:
|
|
108
|
+
markets = fetch_markets(limit=50, search=args.search)
|
|
109
|
+
asset_map = extract_asset_ids(markets)
|
|
110
|
+
if not asset_map:
|
|
111
|
+
print("No markets found for search query.", file=sys.stderr)
|
|
112
|
+
return
|
|
113
|
+
else:
|
|
114
|
+
n = args.markets or 10
|
|
115
|
+
markets = fetch_markets(limit=n)
|
|
116
|
+
asset_map = extract_asset_ids(markets)
|
|
117
|
+
|
|
118
|
+
if not asset_map:
|
|
119
|
+
print("No asset IDs to stream.", file=sys.stderr)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if IS_TTY:
|
|
123
|
+
_pretty(f" Streaming {len(asset_map)} market(s)... (Ctrl+C to stop)\n")
|
|
124
|
+
|
|
125
|
+
# Build handlers
|
|
126
|
+
async def on_book(events: list[BookEvent]) -> None:
|
|
127
|
+
for ev in events:
|
|
128
|
+
label = asset_map.get(ev.asset_id, ev.asset_id[:12])
|
|
129
|
+
if IS_TTY:
|
|
130
|
+
_pretty(f" [book] {label}: {len(ev.bids)} bids, {len(ev.asks)} asks")
|
|
131
|
+
else:
|
|
132
|
+
_json_line({
|
|
133
|
+
"event": "book",
|
|
134
|
+
"asset_id": ev.asset_id,
|
|
135
|
+
"bids": len(ev.bids),
|
|
136
|
+
"asks": len(ev.asks),
|
|
137
|
+
"timestamp": ev.timestamp,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
async def on_price_update(events: list[PolymarketPriceUpdateEvent]) -> None:
|
|
141
|
+
for ev in events:
|
|
142
|
+
label = asset_map.get(ev.asset_id, ev.asset_id[:12])
|
|
143
|
+
if IS_TTY:
|
|
144
|
+
pct = float(ev.price) * 100
|
|
145
|
+
_pretty(f" [price] {label}: {pct:.2f}%")
|
|
146
|
+
else:
|
|
147
|
+
_json_line({
|
|
148
|
+
"event": "price_update",
|
|
149
|
+
"asset_id": ev.asset_id,
|
|
150
|
+
"price": ev.price,
|
|
151
|
+
"midpoint": ev.midpoint,
|
|
152
|
+
"spread": ev.spread,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
async def on_price_change(events: list[PriceChangeEvent]) -> None:
|
|
156
|
+
for ev in events:
|
|
157
|
+
for pc in ev.price_changes:
|
|
158
|
+
label = asset_map.get(pc.asset_id, pc.asset_id[:12])
|
|
159
|
+
if IS_TTY:
|
|
160
|
+
_pretty(
|
|
161
|
+
f" [change] {label}: {pc.side} {pc.price} x{pc.size}"
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
_json_line({
|
|
165
|
+
"event": "price_change",
|
|
166
|
+
"asset_id": pc.asset_id,
|
|
167
|
+
"side": pc.side,
|
|
168
|
+
"price": pc.price,
|
|
169
|
+
"size": pc.size,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
async def on_last_trade(events: list[LastTradePriceEvent]) -> None:
|
|
173
|
+
for ev in events:
|
|
174
|
+
label = asset_map.get(ev.asset_id, ev.asset_id[:12])
|
|
175
|
+
if IS_TTY:
|
|
176
|
+
_pretty(
|
|
177
|
+
f" [trade] {label}: {ev.side} {ev.price} x{ev.size}"
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
_json_line({
|
|
181
|
+
"event": "last_trade",
|
|
182
|
+
"asset_id": ev.asset_id,
|
|
183
|
+
"side": ev.side,
|
|
184
|
+
"price": ev.price,
|
|
185
|
+
"size": ev.size,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
async def on_error(exc: Exception) -> None:
|
|
189
|
+
print(f" [error] {exc}", file=sys.stderr)
|
|
190
|
+
|
|
191
|
+
handlers = WebSocketHandlers(
|
|
192
|
+
on_book=on_book,
|
|
193
|
+
on_price_change=on_price_change,
|
|
194
|
+
on_last_trade_price=on_last_trade,
|
|
195
|
+
on_polymarket_price_update=on_price_update,
|
|
196
|
+
on_error=on_error,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
mgr = WSSubscriptionManager(handlers)
|
|
200
|
+
try:
|
|
201
|
+
await mgr.add_subscriptions(list(asset_map.keys()))
|
|
202
|
+
# Keep running until interrupted
|
|
203
|
+
while True:
|
|
204
|
+
await asyncio.sleep(1)
|
|
205
|
+
finally:
|
|
206
|
+
await mgr.clear_state()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ── Entry point ──────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main() -> None:
|
|
213
|
+
"""CLI entry point. Parses args, dispatches to command handlers."""
|
|
214
|
+
global IS_TTY
|
|
215
|
+
IS_TTY = sys.stdout.isatty()
|
|
216
|
+
|
|
217
|
+
parser = argparse.ArgumentParser(
|
|
218
|
+
prog="polyws",
|
|
219
|
+
description="Polymarket WebSocket streaming CLI",
|
|
220
|
+
)
|
|
221
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
222
|
+
|
|
223
|
+
# markets subcommand
|
|
224
|
+
markets_parser = subparsers.add_parser(
|
|
225
|
+
"markets", help="List active Polymarket markets"
|
|
226
|
+
)
|
|
227
|
+
markets_parser.add_argument("--search", type=str, help="Search markets by text")
|
|
228
|
+
markets_parser.add_argument("--limit", type=int, default=20, help="Max results")
|
|
229
|
+
markets_parser.add_argument(
|
|
230
|
+
"--sort",
|
|
231
|
+
choices=["volume", "liquidity", "newest"],
|
|
232
|
+
default="volume",
|
|
233
|
+
help="Sort order",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# stream subcommand
|
|
237
|
+
stream_parser = subparsers.add_parser(
|
|
238
|
+
"stream", help="Stream real-time market data"
|
|
239
|
+
)
|
|
240
|
+
stream_parser.add_argument("--asset-id", nargs="+", help="Asset ID(s) to stream")
|
|
241
|
+
stream_parser.add_argument("--markets", type=int, help="Top N markets by volume")
|
|
242
|
+
stream_parser.add_argument("--search", type=str, help="Search markets to stream")
|
|
243
|
+
|
|
244
|
+
args = parser.parse_args()
|
|
245
|
+
|
|
246
|
+
if not args.command:
|
|
247
|
+
parser.print_help()
|
|
248
|
+
sys.exit(1)
|
|
249
|
+
|
|
250
|
+
# Clear proxy env vars before WS connections
|
|
251
|
+
for key in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"):
|
|
252
|
+
os.environ.pop(key, None)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
if args.command == "markets":
|
|
256
|
+
_cmd_markets(args)
|
|
257
|
+
elif args.command == "stream":
|
|
258
|
+
asyncio.run(_cmd_stream(args))
|
|
259
|
+
except KeyboardInterrupt:
|
|
260
|
+
if IS_TTY:
|
|
261
|
+
print("\n Stopped.", file=sys.stderr)
|
|
262
|
+
sys.exit(0)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
main()
|
polyws/gamma.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Gamma API helper for fetching Polymarket markets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from urllib.request import Request, urlopen
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
GAMMA_API_URL = "https://gamma-api.polymarket.com/markets"
|
|
11
|
+
|
|
12
|
+
SORT_MAP = {
|
|
13
|
+
"volume": ("volumeNum", "false"),
|
|
14
|
+
"liquidity": ("liquidityNum", "false"),
|
|
15
|
+
"newest": ("startDate", "false"),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def fetch_markets(
|
|
20
|
+
limit: int = 20,
|
|
21
|
+
sort: str = "volume",
|
|
22
|
+
search: str | None = None,
|
|
23
|
+
) -> list[dict]:
|
|
24
|
+
"""Fetch markets from Gamma API.
|
|
25
|
+
|
|
26
|
+
Returns list of market dicts with keys: question, clobTokenIds, volume, etc.
|
|
27
|
+
"""
|
|
28
|
+
order_field, ascending = SORT_MAP.get(sort, SORT_MAP["volume"])
|
|
29
|
+
params = {
|
|
30
|
+
"limit": str(limit),
|
|
31
|
+
"order": order_field,
|
|
32
|
+
"ascending": ascending,
|
|
33
|
+
"active": "true",
|
|
34
|
+
"closed": "false",
|
|
35
|
+
}
|
|
36
|
+
if search:
|
|
37
|
+
params["q"] = search
|
|
38
|
+
|
|
39
|
+
query = "&".join(f"{k}={v}" for k, v in params.items())
|
|
40
|
+
url = f"{GAMMA_API_URL}?{query}"
|
|
41
|
+
req = Request(url, headers={"User-Agent": "polyws/0.1.0"})
|
|
42
|
+
|
|
43
|
+
with urlopen(req) as resp:
|
|
44
|
+
return json.loads(resp.read())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def extract_asset_ids(markets: list[dict]) -> dict[str, str]:
|
|
48
|
+
"""Extract asset_id -> question mapping from market list.
|
|
49
|
+
|
|
50
|
+
Returns dict mapping first clobTokenId to market question.
|
|
51
|
+
"""
|
|
52
|
+
result: dict[str, str] = {}
|
|
53
|
+
for m in markets:
|
|
54
|
+
try:
|
|
55
|
+
token_ids = json.loads(m.get("clobTokenIds", "[]"))
|
|
56
|
+
if token_ids:
|
|
57
|
+
result[token_ids[0]] = m.get("question", "Unknown")
|
|
58
|
+
except (json.JSONDecodeError, IndexError):
|
|
59
|
+
continue
|
|
60
|
+
return result
|