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.
- {pmwallets-0.7.0 → pmwallets-0.8.0}/PKG-INFO +2 -2
- {pmwallets-0.7.0 → pmwallets-0.8.0}/README.md +1 -1
- {pmwallets-0.7.0 → pmwallets-0.8.0}/README.zh.md +1 -1
- {pmwallets-0.7.0 → pmwallets-0.8.0}/pyproject.toml +1 -1
- {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/__init__.py +3 -1
- {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/cli.py +10 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/stream.py +61 -0
- pmwallets-0.8.0/src/pmwallets/version.py +1 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/tests/test_stream.py +48 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/uv.lock +1 -1
- pmwallets-0.7.0/src/pmwallets/version.py +0 -1
- {pmwallets-0.7.0 → pmwallets-0.8.0}/.github/workflows/ci.yml +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/.github/workflows/publish.yml +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/.gitignore +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/LICENSE +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/SKILL.md +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/glama.json +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/SKILL.md +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/client.py +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/market.py +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/mcp.py +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/tools.py +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/types.py +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/watch.py +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/src/pmwallets/webhook.py +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/tests/test_agent.py +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/tests/test_client.py +0 -0
- {pmwallets-0.7.0 → pmwallets-0.8.0}/tests/test_real_socket.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
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"]
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|