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 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
polyws/logger.py ADDED
@@ -0,0 +1,5 @@
1
+ """Logging configuration for polyws."""
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger("polyws")