pmwallets 0.7.0__tar.gz → 0.8.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.
Files changed (29) hide show
  1. {pmwallets-0.7.0 → pmwallets-0.8.0}/PKG-INFO +2 -2
  2. {pmwallets-0.7.0 → pmwallets-0.8.0}/README.md +1 -1
  3. {pmwallets-0.7.0 → pmwallets-0.8.0}/README.zh.md +1 -1
  4. {pmwallets-0.7.0 → pmwallets-0.8.0}/pyproject.toml +1 -1
  5. {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/__init__.py +3 -1
  6. {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/cli.py +10 -0
  7. {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/stream.py +61 -0
  8. pmwallets-0.8.0/src/pmwallets/version.py +1 -0
  9. {pmwallets-0.7.0 → pmwallets-0.8.0}/tests/test_stream.py +48 -0
  10. {pmwallets-0.7.0 → pmwallets-0.8.0}/uv.lock +1 -1
  11. pmwallets-0.7.0/src/pmwallets/version.py +0 -1
  12. {pmwallets-0.7.0 → pmwallets-0.8.0}/.github/workflows/ci.yml +0 -0
  13. {pmwallets-0.7.0 → pmwallets-0.8.0}/.github/workflows/publish.yml +0 -0
  14. {pmwallets-0.7.0 → pmwallets-0.8.0}/.gitignore +0 -0
  15. {pmwallets-0.7.0 → pmwallets-0.8.0}/LICENSE +0 -0
  16. {pmwallets-0.7.0 → pmwallets-0.8.0}/SKILL.md +0 -0
  17. {pmwallets-0.7.0 → pmwallets-0.8.0}/glama.json +0 -0
  18. {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/SKILL.md +0 -0
  19. {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/client.py +0 -0
  20. {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/market.py +0 -0
  21. {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/mcp.py +0 -0
  22. {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/tools.py +0 -0
  23. {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/types.py +0 -0
  24. {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/watch.py +0 -0
  25. {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/webhook.py +0 -0
  26. {pmwallets-0.7.0 → pmwallets-0.8.0}/tests/test_agent.py +0 -0
  27. {pmwallets-0.7.0 → pmwallets-0.8.0}/tests/test_client.py +0 -0
  28. {pmwallets-0.7.0 → pmwallets-0.8.0}/tests/test_real_socket.py +0 -0
  29. {pmwallets-0.7.0 → pmwallets-0.8.0}/tests/test_watch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pmwallets
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Official PMWallets SDK and CLI: Polymarket smart-money leaderboard, real-time wallet fills over WebSocket with gap replay, webhooks, exports — and a CLI + MCP server so AI agents (Claude Code, Codex, Cursor, …) can follow traders and act on their fills.
5
5
  Project-URL: Homepage, https://pmwallets.com/docs
6
6
  Project-URL: Repository, https://github.com/polymarketwallets/pmwallets-python
@@ -46,7 +46,7 @@ big = client.radar(minUsd=25000, smart=True)
46
46
  client.save_signal_rule("signal", {"enabled": True, "scope": "official", "minWallets": 3, "windowMin": 30, "minUsd": 5000, "channels": ["ws", "webhook"]})
47
47
  ```
48
48
 
49
- Live alerts arrive on `wss://api.pmwallets.com/v1/signals/ws` as `{type: 'signal' | 'radar', data}`. Amounts keep two significant figures, times the minute, and traders are named by handle.
49
+ For live alerts use `SignalStream(client, on_alert).start()`, or `pmwallets alerts` on the command line. Amounts keep two significant figures, times the minute, and traders are named by handle.
50
50
 
51
51
  ## Usage
52
52
 
@@ -28,7 +28,7 @@ big = client.radar(minUsd=25000, smart=True)
28
28
  client.save_signal_rule("signal", {"enabled": True, "scope": "official", "minWallets": 3, "windowMin": 30, "minUsd": 5000, "channels": ["ws", "webhook"]})
29
29
  ```
30
30
 
31
- Live alerts arrive on `wss://api.pmwallets.com/v1/signals/ws` as `{type: 'signal' | 'radar', data}`. Amounts keep two significant figures, times the minute, and traders are named by handle.
31
+ For live alerts use `SignalStream(client, on_alert).start()`, or `pmwallets alerts` on the command line. Amounts keep two significant figures, times the minute, and traders are named by handle.
32
32
 
33
33
  ## Usage
34
34
 
@@ -25,7 +25,7 @@ big = client.radar(minUsd=25000, smart=True)
25
25
  client.save_signal_rule("signal", {"enabled": True, "scope": "official", "minWallets": 3, "windowMin": 30, "minUsd": 5000, "channels": ["ws", "webhook"]})
26
26
  ```
27
27
 
28
- 实时告警走 `wss://api.pmwallets.com/v1/signals/ws`,帧为 `{type: 'signal' | 'radar', data}`。金额保留两位有效数字,时间到分钟,交易者只给代号。
28
+ 实时告警用 `SignalStream(client, on_alert).start()`,或命令行 `pmwallets alerts`。金额保留两位有效数字,时间到分钟,交易者只给代号。
29
29
 
30
30
  ## 包含什么
31
31
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pmwallets"
7
- version = "0.7.0"
7
+ version = "0.8.0"
8
8
  description = "Official PMWallets SDK and CLI: Polymarket smart-money leaderboard, real-time wallet fills over WebSocket with gap replay, webhooks, exports — and a CLI + MCP server so AI agents (Claude Code, Codex, Cursor, …) can follow traders and act on their fills."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -3,7 +3,7 @@
3
3
  from .client import DEFAULT_BASE_URL, AsyncClient, Client, PmwError
4
4
  from .market import MarketInfo, MarketLookup, micro_to_decimal
5
5
  from .mcp import MCP_PROTOCOL_VERSIONS, McpTool, serve_mcp
6
- from .stream import CLOSE_API_SEAT_TAKEN, CLOSE_SERVICE_RESTART, FileStateStore, FillMeta, FillStream, MemoryStateStore, StreamState, UpgradeRefused, websockets_connector
6
+ from .stream import CLOSE_API_SEAT_TAKEN, CLOSE_SERVICE_RESTART, CLOSE_SIGNALS_REPLACED, SignalStream, FileStateStore, FillMeta, FillStream, MemoryStateStore, StreamState, UpgradeRefused, websockets_connector
7
7
  from .tools import MCP_INSTRUCTIONS, CursorFile, describe_fill, new_fills, pmwallets_tools
8
8
  from .types import Fill, FillCursor, FillsPage
9
9
  from .version import VERSION
@@ -11,6 +11,8 @@ from .watch import Watcher
11
11
  from .webhook import verify_webhook
12
12
 
13
13
  __all__ = [
14
+ "SignalStream",
15
+ "CLOSE_SIGNALS_REPLACED",
14
16
  "DEFAULT_BASE_URL", "AsyncClient", "Client", "PmwError",
15
17
  "CLOSE_API_SEAT_TAKEN", "CLOSE_SERVICE_RESTART", "FileStateStore", "FillMeta", "FillStream", "MemoryStateStore", "StreamState", "UpgradeRefused", "websockets_connector",
16
18
  "Fill", "FillCursor", "FillsPage", "verify_webhook",
@@ -21,6 +21,7 @@ import time
21
21
  from importlib import resources
22
22
  from typing import Any, Optional, Union
23
23
 
24
+ from .stream import SignalStream
24
25
  from .client import DEFAULT_BASE_URL, AsyncClient
25
26
  from .market import MarketLookup
26
27
  from .mcp import serve_mcp
@@ -49,6 +50,7 @@ Set PMW_API_KEY (create one on https://pmwallets.com/keys). Every command prints
49
50
  pmwallets account plan and balance
50
51
  pmwallets signals [--source mine] [--category sports] [--q fed] smart-money signals (Pro and Max)
51
52
  pmwallets radar [--minUsd 25000] [--side BUY] [--smart] large fills, whole market (Pro and Max)
53
+ pmwallets alerts live alerts of your rules, one JSON line each (Pro and Max)
52
54
  pmwallets mcp MCP server on stdio (tools: leaderboard, entity, subscribe, new_fills, …)
53
55
  pmwallets skill print the guide for AI agents (SKILL.md)
54
56
 
@@ -123,6 +125,14 @@ async def _main(argv: list[str]) -> int:
123
125
  markets = MarketLookup()
124
126
  await serve_mcp(name="pmwallets", version=VERSION, tools=pmwallets_tools(client, markets=markets), instructions=MCP_INSTRUCTIONS)
125
127
  return 0
128
+ if cmd == "alerts":
129
+ if client is None:
130
+ raise ValueError("set PMW_API_KEY (create a key on https://pmwallets.com/keys)")
131
+ stream = SignalStream(client, lambda a: print(json.dumps(a), flush=True),
132
+ on_event=lambda e: print(e["reason"], file=sys.stderr) if e["type"] == "stopped" else None)
133
+ stream.start()
134
+ await stream.wait()
135
+ return 0
126
136
  if cmd == "watch":
127
137
  if flags.get("exec") is True:
128
138
  raise ValueError("--exec needs a command, e.g. --exec \"claude -p '…'\"")
@@ -364,3 +364,64 @@ class FillStream:
364
364
  self.state.block = fill["block"]
365
365
  self.state.logIndex = fill["logIndex"]
366
366
  return True
367
+
368
+
369
+ # this socket was replaced by a newer one past the account's limit: reconnecting would evict another
370
+ CLOSE_SIGNALS_REPLACED = 4412
371
+
372
+
373
+ class SignalStream:
374
+ """The live alerts of your rules (Pro and Max): connects, reconnects with backoff, stops when told not to come back.
375
+ Best effort: GET /v1/signals and /v1/radar hold what a gap missed. `on_alert` gets {"type": "signal" | "radar", "data": ...}."""
376
+
377
+ def __init__(self, client: AsyncClient, on_alert: Callable[[dict[str, Any]], Any], on_event: Optional[OnEvent] = None,
378
+ connector: Optional[Connector] = None, max_backoff: float = 15.0) -> None:
379
+ self._client, self._on_alert, self._on_event = client, on_alert, on_event or (lambda e: None)
380
+ self._connector = connector or websockets_connector()
381
+ self._max_backoff = max_backoff
382
+ self._task: Optional[asyncio.Task[None]] = None
383
+
384
+ def start(self) -> None:
385
+ self._task = asyncio.create_task(self._run())
386
+
387
+ async def stop(self) -> None:
388
+ if self._task:
389
+ self._task.cancel()
390
+ with contextlib.suppress(asyncio.CancelledError):
391
+ await self._task
392
+
393
+ async def wait(self) -> None:
394
+ if self._task:
395
+ await self._task
396
+
397
+ async def _run(self) -> None:
398
+ backoff = 1.0
399
+ while True:
400
+ code = None
401
+ try:
402
+ async with self._connector(self._client.signals_ws_url, {"x-api-key": self._client.api_key}) as ws:
403
+ self._on_event({"type": "connected"})
404
+ backoff = 1.0
405
+ async for raw in ws:
406
+ try:
407
+ f = json.loads(raw)
408
+ except ValueError:
409
+ continue
410
+ if isinstance(f, dict) and f.get("type") in ("signal", "radar") and f.get("data"):
411
+ r = self._on_alert({"type": f["type"], "data": f["data"]})
412
+ if asyncio.iscoroutine(r):
413
+ await r
414
+ code = getattr(ws, "close_code", None)
415
+ except UpgradeRefused as e:
416
+ if e.status_code in (401, 403):
417
+ reason = "signals are part of the Pro and Max plans" if e.status_code == 403 else "the API key was refused"
418
+ return self._on_event({"type": "stopped", "reason": reason})
419
+ except asyncio.CancelledError:
420
+ raise
421
+ except Exception as e:
422
+ code = getattr(getattr(e, "rcvd", None), "code", None)
423
+ self._on_event({"type": "disconnected", "code": code})
424
+ if code == CLOSE_SIGNALS_REPLACED:
425
+ return self._on_event({"type": "stopped", "reason": "replaced by a newer connection"})
426
+ await asyncio.sleep(0 if code == CLOSE_SERVICE_RESTART else backoff)
427
+ backoff = min(backoff * 2, self._max_backoff)
@@ -0,0 +1 @@
1
+ VERSION = "0.8.0"
@@ -263,3 +263,51 @@ async def test_file_state_store_roundtrip(tmp_path):
263
263
  await store.save(StreamState("s", 3, 9, 2))
264
264
  assert await store.load() == StreamState("s", 3, 9, 2)
265
265
  assert json.loads((tmp_path / "sub" / "state.json").read_text()) == {"session": "s", "seq": 3, "block": 9, "logIndex": 2}
266
+
267
+
268
+ class _Sock:
269
+ def __init__(self, frames, code=None):
270
+ self.frames, self.close_code = frames, code
271
+
272
+ def __aiter__(self):
273
+ async def gen():
274
+ for f in self.frames:
275
+ yield f
276
+ return gen()
277
+
278
+
279
+ def _connector(socks, urls):
280
+ @contextlib.asynccontextmanager
281
+ async def connect(url, headers):
282
+ urls.append(url)
283
+ s = socks.pop(0)
284
+ if isinstance(s, Exception):
285
+ raise s
286
+ yield s
287
+ return connect
288
+
289
+
290
+ class _Key:
291
+ api_key = "pmw_a_b"
292
+ signals_ws_url = "wss://x/v1/signals/ws"
293
+
294
+
295
+ async def test_signal_stream_delivers_alerts_and_stops_when_replaced():
296
+ from pmwallets import SignalStream
297
+ seen, urls, events = [], [], []
298
+ socks = [_Sock(['{"type":"hello"}', "not json", '{"type":"radar","data":{"usd":52000}}'], 1006), _Sock([], 4412)]
299
+ s = SignalStream(_Key(), seen.append, events.append, connector=_connector(socks, urls), max_backoff=0.01)
300
+ s.start()
301
+ await asyncio.wait_for(s.wait(), 5)
302
+ assert seen == [{"type": "radar", "data": {"usd": 52000}}]
303
+ assert len(urls) == 2 and events[-1]["type"] == "stopped"
304
+
305
+
306
+ async def test_signal_stream_stops_on_a_plan_without_signals():
307
+ from pmwallets import SignalStream
308
+ from pmwallets.stream import UpgradeRefused
309
+ urls, events = [], []
310
+ s = SignalStream(_Key(), lambda a: None, events.append, connector=_connector([UpgradeRefused(403)], urls))
311
+ s.start()
312
+ await asyncio.wait_for(s.wait(), 5)
313
+ assert len(urls) == 1 and "Pro and Max" in events[-1]["reason"]
@@ -134,7 +134,7 @@ wheels = [
134
134
 
135
135
  [[package]]
136
136
  name = "pmwallets"
137
- version = "0.7.0"
137
+ version = "0.8.0"
138
138
  source = { editable = "." }
139
139
  dependencies = [
140
140
  { name = "httpx" },
@@ -1 +0,0 @@
1
- VERSION = "0.7.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes