pmquant 0.3.0__tar.gz → 0.4.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.
- {pmquant-0.3.0 → pmquant-0.4.0}/.github/workflows/canary.yml +2 -2
- {pmquant-0.3.0 → pmquant-0.4.0}/.github/workflows/publish.yml +2 -2
- {pmquant-0.3.0 → pmquant-0.4.0}/.github/workflows/test.yml +5 -4
- {pmquant-0.3.0 → pmquant-0.4.0}/.gitignore +1 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/CHANGELOG.md +24 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/PKG-INFO +11 -1
- {pmquant-0.3.0 → pmquant-0.4.0}/README.md +2 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/dash/bot_dash.py +4 -4
- {pmquant-0.3.0 → pmquant-0.4.0}/pyproject.toml +18 -2
- {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/__init__.py +2 -2
- {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/data.py +70 -34
- {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/doctor.py +23 -12
- {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/executor.py +50 -31
- {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/mcp.py +20 -14
- pmquant-0.4.0/src/pmq/py.typed +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/tests/test_data.py +82 -0
- pmquant-0.4.0/tests/test_doctor.py +160 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/tests/test_executor.py +101 -0
- pmquant-0.4.0/tests/test_mcp.py +133 -0
- pmquant-0.3.0/tests/test_doctor.py +0 -26
- pmquant-0.3.0/tests/test_mcp.py +0 -38
- {pmquant-0.3.0 → pmquant-0.4.0}/AGENTS.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/CLAUDE.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/CONTRIBUTING.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/LICENSE +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/SECURITY.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/README.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/bot.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/dash/dash.html +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/pmq-bot.service +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/strategy.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/docs/assets/pmq-doctor.svg +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/docs/recipes.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/docs/rounding-study.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/docs/war-story.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/examples/fak_buy_guarded.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/examples/read_market.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/llms.txt +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/exceptions.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/tests/test_canary_live.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.0}/tests/test_template_engine.py +0 -0
|
@@ -7,12 +7,13 @@ jobs:
|
|
|
7
7
|
runs-on: ubuntu-latest
|
|
8
8
|
strategy:
|
|
9
9
|
matrix:
|
|
10
|
-
python-version: ["3.10", "3.12"]
|
|
10
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
11
11
|
steps:
|
|
12
|
-
- uses: actions/checkout@
|
|
13
|
-
- uses: actions/setup-python@
|
|
12
|
+
- uses: actions/checkout@v5
|
|
13
|
+
- uses: actions/setup-python@v6
|
|
14
14
|
with:
|
|
15
15
|
python-version: ${{ matrix.python-version }}
|
|
16
16
|
- run: pip install -e ".[dev]"
|
|
17
17
|
- run: ruff check .
|
|
18
|
-
- run:
|
|
18
|
+
- run: mypy
|
|
19
|
+
- run: pytest -q --cov=pmq --cov-fail-under=85
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 (2026-07-03)
|
|
4
|
+
|
|
5
|
+
* Typing: the whole public API is annotated and ships a `py.typed` marker;
|
|
6
|
+
`mypy --strict` runs in CI. New exported types `ParsedMarket` and
|
|
7
|
+
`BookMeta` (TypedDicts) describe what `parse_market` and `book_meta`
|
|
8
|
+
return.
|
|
9
|
+
* Builder attribution: the code now rides explicitly inside EVERY order's
|
|
10
|
+
args (market and limit paths) in addition to the client-level
|
|
11
|
+
BuilderConfig, and tests pin all three layers: executor args, real client
|
|
12
|
+
construction, and the dependency's own config injection. Disclosure and
|
|
13
|
+
one-line opt-out unchanged.
|
|
14
|
+
* Tests: 97 (from 39). Executable table of the fail-closed fill contract
|
|
15
|
+
(one row per possible exchange outcome, both order paths), deep pmq-doctor
|
|
16
|
+
scenarios with mocked RPC and CLOB (sig_type advice, alternative sig_type
|
|
17
|
+
probing, market section), MCP tool coverage including the gated live
|
|
18
|
+
tools. Coverage gate at 85% in CI.
|
|
19
|
+
* pmq-doctor fixes: an RPC failure now fails the run (it used to leave the
|
|
20
|
+
verdict green), and a non-numeric POLY_SIG_TYPE is diagnosed instead of
|
|
21
|
+
crashing.
|
|
22
|
+
* CI: test matrix extended to Python 3.10 through 3.14, classifiers updated
|
|
23
|
+
accordingly.
|
|
24
|
+
* Template: the dash brands itself pmq-bot-dash, matching the shipped
|
|
25
|
+
pmq-bot.service unit name.
|
|
26
|
+
|
|
3
27
|
## 0.3.0 (2026-07-03)
|
|
4
28
|
|
|
5
29
|
* New: `pmq-doctor`, a read-only diagnosis command for Polymarket V2 setups.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pmquant
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Fail-closed execution and market-data layer for Polymarket CLOB V2: local signing, confirmed fills only, fee-correct math, working deposit-wallet (POLY_1271) support.
|
|
5
5
|
Project-URL: Homepage, https://github.com/crp4222/pmq
|
|
6
6
|
Project-URL: Issues, https://github.com/crp4222/pmq/issues
|
|
@@ -13,11 +13,19 @@ Classifier: Intended Audience :: Developers
|
|
|
13
13
|
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
21
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
22
|
+
Classifier: Typing :: Typed
|
|
17
23
|
Requires-Python: >=3.10
|
|
18
24
|
Requires-Dist: py-clob-client-v2<2,>=1.0.2
|
|
19
25
|
Provides-Extra: dev
|
|
20
26
|
Requires-Dist: mcp>=1.2; extra == 'dev'
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
21
29
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
22
30
|
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
23
31
|
Provides-Extra: mcp
|
|
@@ -29,6 +37,8 @@ Description-Content-Type: text/markdown
|
|
|
29
37
|
[](https://pypi.org/project/pmquant/)
|
|
30
38
|
[](https://github.com/crp4222/pmq/actions/workflows/test.yml)
|
|
31
39
|
[](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
|
|
40
|
+
[](.github/workflows/test.yml)
|
|
41
|
+
[](pyproject.toml)
|
|
32
42
|
[](LICENSE)
|
|
33
43
|
|
|
34
44
|
Fail-closed execution and market data for **Polymarket CLOB V2**, in Python.
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
[](https://pypi.org/project/pmquant/)
|
|
4
4
|
[](https://github.com/crp4222/pmq/actions/workflows/test.yml)
|
|
5
5
|
[](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
|
|
6
|
+
[](.github/workflows/test.yml)
|
|
7
|
+
[](pyproject.toml)
|
|
6
8
|
[](LICENSE)
|
|
7
9
|
|
|
8
10
|
Fail-closed execution and market data for **Polymarket CLOB V2**, in Python.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Design constraints:
|
|
5
5
|
- stdlib only (http.server), one process, a few MB of RSS: safe on the Pi
|
|
6
|
-
- READ ONLY: parses bot_runs CSVs, systemctl show and the
|
|
6
|
+
- READ ONLY: parses bot_runs CSVs, systemctl show and the bot's journal;
|
|
7
7
|
never reads live.env, never talks to any exchange API, no secrets
|
|
8
8
|
- the phone does the rendering; this process only ships small JSON + one
|
|
9
9
|
static HTML file (cached in memory, CSVs re-parsed only on mtime change)
|
|
@@ -67,7 +67,7 @@ def _run(cmd):
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def service_state():
|
|
70
|
-
"""
|
|
70
|
+
"""bot systemd state + last collateral line from its journal."""
|
|
71
71
|
with _lock:
|
|
72
72
|
cached = _cache.get("svc")
|
|
73
73
|
if cached and time.time() - cached[0] < SVC_CACHE_S:
|
|
@@ -141,7 +141,7 @@ def api_payload():
|
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
class Handler(BaseHTTPRequestHandler):
|
|
144
|
-
server_version = "
|
|
144
|
+
server_version = "pmq-bot-dash"
|
|
145
145
|
|
|
146
146
|
def _send(self, code, ctype, body):
|
|
147
147
|
self.send_response(code)
|
|
@@ -172,5 +172,5 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
172
172
|
|
|
173
173
|
if __name__ == "__main__":
|
|
174
174
|
srv = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
|
|
175
|
-
print(f"
|
|
175
|
+
print(f"pmq-bot-dash listening on :{PORT}", flush=True)
|
|
176
176
|
srv.serve_forever()
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pmquant"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Fail-closed execution and market-data layer for Polymarket CLOB V2: local signing, confirmed fills only, fee-correct math, working deposit-wallet (POLY_1271) support."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -22,7 +22,13 @@ classifiers = [
|
|
|
22
22
|
"Intended Audience :: Financial and Insurance Industry",
|
|
23
23
|
"License :: OSI Approved :: MIT License",
|
|
24
24
|
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
25
30
|
"Topic :: Office/Business :: Financial :: Investment",
|
|
31
|
+
"Typing :: Typed",
|
|
26
32
|
]
|
|
27
33
|
|
|
28
34
|
[project.urls]
|
|
@@ -31,7 +37,7 @@ Issues = "https://github.com/crp4222/pmq/issues"
|
|
|
31
37
|
|
|
32
38
|
[project.optional-dependencies]
|
|
33
39
|
mcp = ["mcp>=1.2"]
|
|
34
|
-
dev = ["pytest>=8", "mcp>=1.2", "ruff>=0.6"]
|
|
40
|
+
dev = ["pytest>=8", "pytest-cov>=5", "mcp>=1.2", "ruff>=0.6", "mypy>=1.10"]
|
|
35
41
|
|
|
36
42
|
[project.scripts]
|
|
37
43
|
pmq-mcp = "pmq.mcp:main"
|
|
@@ -40,6 +46,16 @@ pmq-doctor = "pmq.doctor:main"
|
|
|
40
46
|
[tool.hatch.build.targets.wheel]
|
|
41
47
|
packages = ["src/pmq"]
|
|
42
48
|
|
|
49
|
+
[tool.mypy]
|
|
50
|
+
python_version = "3.10"
|
|
51
|
+
strict = true
|
|
52
|
+
files = ["src/pmq"]
|
|
53
|
+
|
|
54
|
+
[[tool.mypy.overrides]]
|
|
55
|
+
module = ["py_clob_client_v2.*", "eth_account.*"]
|
|
56
|
+
ignore_missing_imports = true
|
|
57
|
+
follow_imports = "skip"
|
|
58
|
+
|
|
43
59
|
[tool.ruff]
|
|
44
60
|
line-length = 100
|
|
45
61
|
target-version = "py310"
|
|
@@ -25,7 +25,7 @@ from .data import (
|
|
|
25
25
|
)
|
|
26
26
|
from .exceptions import IntrospectionMismatch, OrderUncertain, PmqError
|
|
27
27
|
|
|
28
|
-
__version__ = "0.
|
|
28
|
+
__version__ = "0.4.0"
|
|
29
29
|
__all__ = [
|
|
30
30
|
"FEE_RATES", "band_ask_depth_usd", "best_bid_ask", "book_inferred_winner",
|
|
31
31
|
"book_meta", "event_markets", "fee", "get_book", "get_market", "get_tape",
|
|
@@ -36,7 +36,7 @@ __all__ = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def __getattr__(name):
|
|
39
|
+
def __getattr__(name: str) -> object:
|
|
40
40
|
# Lazy import: the data layer must stay usable without py-clob-client-v2.
|
|
41
41
|
if name in ("PolymarketExecutor", "Fill", "DEFAULT_BUILDER_CODE"):
|
|
42
42
|
from . import executor
|
|
@@ -19,10 +19,15 @@ Facts this module encodes (verified live, July 2026):
|
|
|
19
19
|
see :data:`FEE_RATES`. Makers always pay zero. The ``maker/taker_base_fee``
|
|
20
20
|
of 1000 bps seen in API responses is an on-chain CAP, never the charge.
|
|
21
21
|
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
22
24
|
import calendar
|
|
23
25
|
import json
|
|
24
26
|
import time
|
|
25
27
|
import urllib.request
|
|
28
|
+
from typing import Any, Callable, TypedDict
|
|
29
|
+
|
|
30
|
+
Logger = Callable[[str], None]
|
|
26
31
|
|
|
27
32
|
UA = {"User-Agent": "Mozilla/5.0"}
|
|
28
33
|
GAMMA = "https://gamma-api.polymarket.com"
|
|
@@ -31,7 +36,7 @@ DATA = "https://data-api.polymarket.com"
|
|
|
31
36
|
|
|
32
37
|
#: Official taker fee rates per market category (docs.polymarket.com/trading/fees,
|
|
33
38
|
#: fetched 2026-07-03). Fee in $ = rate * p * (1 - p) * shares. Makers pay 0.
|
|
34
|
-
FEE_RATES = {
|
|
39
|
+
FEE_RATES: dict[str, float] = {
|
|
35
40
|
"crypto": 0.07,
|
|
36
41
|
"sports": 0.03,
|
|
37
42
|
"finance": 0.04,
|
|
@@ -45,7 +50,28 @@ FEE_RATES = {
|
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
class ParsedMarket(TypedDict):
|
|
54
|
+
"""Normalized view of a Gamma market object, see :func:`parse_market`."""
|
|
55
|
+
condition_id: str | None
|
|
56
|
+
slug: str | None
|
|
57
|
+
token_a: str
|
|
58
|
+
token_b: str
|
|
59
|
+
outcome_a: str
|
|
60
|
+
outcome_b: str
|
|
61
|
+
outcome_prices_raw: Any
|
|
62
|
+
idx_a: int
|
|
63
|
+
end_ts: int | None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BookMeta(TypedDict):
|
|
67
|
+
"""Exchange metadata riding on a book response, see :func:`book_meta`."""
|
|
68
|
+
min_order_size: float | None
|
|
69
|
+
tick_size: float | None
|
|
70
|
+
neg_risk: bool | None
|
|
71
|
+
last_trade_price: float | None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def fee(price: float, shares: float, rate: float = FEE_RATES["crypto"]) -> float:
|
|
49
75
|
"""Taker fee in $ under the current schedule. Makers pay zero.
|
|
50
76
|
|
|
51
77
|
The fee peaks at price 0.50 and vanishes toward 0 and 1: a taker fill at
|
|
@@ -54,7 +80,8 @@ def fee(price, shares, rate=FEE_RATES["crypto"]):
|
|
|
54
80
|
return rate * price * (1.0 - price) * shares
|
|
55
81
|
|
|
56
82
|
|
|
57
|
-
def http_get_json(url, retries=3, timeout=10,
|
|
83
|
+
def http_get_json(url: str, retries: int = 3, timeout: float = 10,
|
|
84
|
+
logger: Logger | None = None) -> Any:
|
|
58
85
|
"""GET a JSON document with linear backoff. Returns None on final failure."""
|
|
59
86
|
for i in range(retries):
|
|
60
87
|
try:
|
|
@@ -70,18 +97,20 @@ def http_get_json(url, retries=3, timeout=10, logger=None):
|
|
|
70
97
|
return None
|
|
71
98
|
|
|
72
99
|
|
|
73
|
-
def get_market(slug, logger=None):
|
|
100
|
+
def get_market(slug: str, logger: Logger | None = None) -> dict[str, Any] | None:
|
|
74
101
|
"""Gamma market object for a slug, falling back to /events for expired ones."""
|
|
75
102
|
data = http_get_json(f"{GAMMA}/markets?slug={slug}", logger=logger)
|
|
76
103
|
if data:
|
|
77
|
-
|
|
104
|
+
first: dict[str, Any] = data[0]
|
|
105
|
+
return first
|
|
78
106
|
ev = http_get_json(f"{GAMMA}/events?slug={slug}", logger=logger)
|
|
79
107
|
if ev and ev[0].get("markets"):
|
|
80
|
-
|
|
108
|
+
fallback: dict[str, Any] = ev[0]["markets"][0]
|
|
109
|
+
return fallback
|
|
81
110
|
return None
|
|
82
111
|
|
|
83
112
|
|
|
84
|
-
def _end_ts(m):
|
|
113
|
+
def _end_ts(m: dict[str, Any]) -> int | None:
|
|
85
114
|
"""Market close time as unix epoch, or None. Gamma sends UTC ISO 8601."""
|
|
86
115
|
raw = m.get("endDate") or m.get("endDateIso") or ""
|
|
87
116
|
for fmt in ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S.%fZ"):
|
|
@@ -92,7 +121,8 @@ def _end_ts(m):
|
|
|
92
121
|
return None
|
|
93
122
|
|
|
94
123
|
|
|
95
|
-
def parse_market(m,
|
|
124
|
+
def parse_market(m: dict[str, Any] | None, outcome_a: str | None = None,
|
|
125
|
+
outcome_b: str | None = None) -> ParsedMarket | None:
|
|
96
126
|
"""Extract condition id and outcome token ids from a Gamma market object.
|
|
97
127
|
|
|
98
128
|
Works on ANY binary market: politics, sports, crypto, whatever the
|
|
@@ -104,26 +134,26 @@ def parse_market(m, outcome_a=None, outcome_b=None):
|
|
|
104
134
|
if not m:
|
|
105
135
|
return None
|
|
106
136
|
try:
|
|
107
|
-
outcomes = json.loads(m["outcomes"]) if isinstance(m.get("outcomes"), str) else m
|
|
108
|
-
token_ids = json.loads(m["clobTokenIds"]) if isinstance(m.get("clobTokenIds"), str) else m
|
|
137
|
+
outcomes: Any = json.loads(m["outcomes"]) if isinstance(m.get("outcomes"), str) else m["outcomes"]
|
|
138
|
+
token_ids: Any = json.loads(m["clobTokenIds"]) if isinstance(m.get("clobTokenIds"), str) else m["clobTokenIds"]
|
|
109
139
|
a = outcomes.index(outcome_a) if outcome_a else 0
|
|
110
140
|
b = outcomes.index(outcome_b) if outcome_b else (1 if a == 0 else 0)
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
141
|
+
return ParsedMarket(
|
|
142
|
+
condition_id=m.get("conditionId"),
|
|
143
|
+
slug=m.get("slug"),
|
|
144
|
+
token_a=token_ids[a],
|
|
145
|
+
token_b=token_ids[b],
|
|
146
|
+
outcome_a=outcomes[a],
|
|
147
|
+
outcome_b=outcomes[b],
|
|
148
|
+
outcome_prices_raw=m.get("outcomePrices"),
|
|
149
|
+
idx_a=int(a),
|
|
150
|
+
end_ts=_end_ts(m),
|
|
151
|
+
)
|
|
122
152
|
except Exception:
|
|
123
153
|
return None
|
|
124
154
|
|
|
125
155
|
|
|
126
|
-
def resolved_winner(pm):
|
|
156
|
+
def resolved_winner(pm: ParsedMarket | None) -> str | None:
|
|
127
157
|
"""Winning outcome name from settled Gamma outcomePrices; None if unsettled."""
|
|
128
158
|
if not pm:
|
|
129
159
|
return None
|
|
@@ -139,12 +169,14 @@ def resolved_winner(pm):
|
|
|
139
169
|
return None
|
|
140
170
|
|
|
141
171
|
|
|
142
|
-
def get_book(token_id, logger=None):
|
|
172
|
+
def get_book(token_id: str, logger: Logger | None = None) -> dict[str, Any] | None:
|
|
143
173
|
"""Real-time CLOB book. THE live data source; never the trade tape."""
|
|
144
|
-
|
|
174
|
+
book: dict[str, Any] | None = http_get_json(f"{CLOB}/book?token_id={token_id}", logger=logger)
|
|
175
|
+
return book
|
|
145
176
|
|
|
146
177
|
|
|
147
|
-
def best_bid_ask(book
|
|
178
|
+
def best_bid_ask(book: dict[str, Any] | None,
|
|
179
|
+
) -> tuple[float | None, float | None, float | None, float | None]:
|
|
148
180
|
"""(best_bid, bid_size, best_ask, ask_size), sizes summed at the level."""
|
|
149
181
|
if not book:
|
|
150
182
|
return None, None, None, None
|
|
@@ -157,12 +189,13 @@ def best_bid_ask(book):
|
|
|
157
189
|
return bb, bb_sz, ba, ba_sz
|
|
158
190
|
|
|
159
191
|
|
|
160
|
-
def book_meta(book):
|
|
192
|
+
def book_meta(book: dict[str, Any] | None) -> BookMeta:
|
|
161
193
|
"""Exchange metadata riding on the book response: per-market minimum
|
|
162
194
|
order size (shares), tick size, neg_risk flag, last trade price. Read
|
|
163
195
|
these from the live book instead of hardcoding exchange rules."""
|
|
164
196
|
b = book or {}
|
|
165
|
-
|
|
197
|
+
|
|
198
|
+
def _f(k: str) -> float | None:
|
|
166
199
|
try:
|
|
167
200
|
return float(b[k])
|
|
168
201
|
except (KeyError, TypeError, ValueError):
|
|
@@ -171,14 +204,15 @@ def book_meta(book):
|
|
|
171
204
|
"neg_risk": b.get("neg_risk"), "last_trade_price": _f("last_trade_price")}
|
|
172
205
|
|
|
173
206
|
|
|
174
|
-
def band_ask_depth_usd(book, lo, hi):
|
|
207
|
+
def band_ask_depth_usd(book: dict[str, Any] | None, lo: float, hi: float) -> float:
|
|
175
208
|
"""Total $ notional of asks resting within [lo, hi]."""
|
|
176
209
|
asks = (book or {}).get("asks") or []
|
|
177
210
|
return round(sum(float(a["price"]) * float(a["size"]) for a in asks
|
|
178
211
|
if lo <= float(a["price"]) <= hi), 2)
|
|
179
212
|
|
|
180
213
|
|
|
181
|
-
def book_inferred_winner(bid_a, bid_b,
|
|
214
|
+
def book_inferred_winner(bid_a: float | None, bid_b: float | None,
|
|
215
|
+
threshold: float = 0.90) -> str | None:
|
|
182
216
|
"""Winner from a last pre-close book snapshot; None if ambiguous.
|
|
183
217
|
|
|
184
218
|
Use when Gamma settlement lags: a side whose BID is pinned at or above
|
|
@@ -191,14 +225,14 @@ def book_inferred_winner(bid_a, bid_b, threshold=0.90):
|
|
|
191
225
|
return None
|
|
192
226
|
|
|
193
227
|
|
|
194
|
-
def event_markets(slug, logger=None):
|
|
228
|
+
def event_markets(slug: str, logger: Logger | None = None) -> list[ParsedMarket]:
|
|
195
229
|
"""All binary markets of one event (multi-outcome events like elections
|
|
196
230
|
or tournaments are one binary market per candidate). Returns a list of
|
|
197
231
|
:func:`parse_market` dicts; unparseable members are skipped."""
|
|
198
232
|
ev = http_get_json(f"{GAMMA}/events?slug={slug}", logger=logger)
|
|
199
233
|
if not ev:
|
|
200
234
|
return []
|
|
201
|
-
out = []
|
|
235
|
+
out: list[ParsedMarket] = []
|
|
202
236
|
for m in ev[0].get("markets") or []:
|
|
203
237
|
pm = parse_market(m)
|
|
204
238
|
if pm:
|
|
@@ -206,7 +240,8 @@ def event_markets(slug, logger=None):
|
|
|
206
240
|
return out
|
|
207
241
|
|
|
208
242
|
|
|
209
|
-
def positions(user_address, logger=None,
|
|
243
|
+
def positions(user_address: str, logger: Logger | None = None,
|
|
244
|
+
limit: int = 200) -> list[dict[str, Any]]:
|
|
210
245
|
"""Current holdings of a wallet per the data-api (public, ~1 min lag).
|
|
211
246
|
Answers "what do I hold?" after fills: list of dicts with asset,
|
|
212
247
|
conditionId, size, avgPrice, currentValue and friends."""
|
|
@@ -214,13 +249,14 @@ def positions(user_address, logger=None, limit=200):
|
|
|
214
249
|
logger=logger) or []
|
|
215
250
|
|
|
216
251
|
|
|
217
|
-
def get_tape(condition_id, since_ts, max_pages=4,
|
|
252
|
+
def get_tape(condition_id: str, since_ts: float, max_pages: int = 4,
|
|
253
|
+
logger: Logger | None = None) -> list[dict[str, Any]]:
|
|
218
254
|
"""Complete trade tape for a closed market (paginated, newest first).
|
|
219
255
|
|
|
220
256
|
OFFLINE USE ONLY: the indexer lags matching by 1 to 3 minutes; call at
|
|
221
257
|
least 5 minutes after close or you will score against missing fills.
|
|
222
258
|
"""
|
|
223
|
-
out = []
|
|
259
|
+
out: list[dict[str, Any]] = []
|
|
224
260
|
for page in range(max_pages):
|
|
225
261
|
batch = http_get_json(
|
|
226
262
|
f"{DATA}/trades?market={condition_id}&limit=500&offset={page*500}",
|
|
@@ -6,11 +6,14 @@ while funds sit on-chain, drifted client surface, per-market minimums.
|
|
|
6
6
|
Read-only: derives addresses, calls public RPC and CLOB endpoints. Never
|
|
7
7
|
prints or transmits the private key. Exit code 0 when everything is green.
|
|
8
8
|
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
9
11
|
import inspect
|
|
10
12
|
import json
|
|
11
13
|
import os
|
|
12
14
|
import sys
|
|
13
15
|
import urllib.request
|
|
16
|
+
from typing import Any
|
|
14
17
|
|
|
15
18
|
from .data import best_bid_ask, book_meta, fee, get_book, get_market, parse_market
|
|
16
19
|
|
|
@@ -20,10 +23,10 @@ RPCS = ["https://polygon-rpc.com", "https://polygon-bor-rpc.publicnode.com",
|
|
|
20
23
|
GREEN, RED, WARN = "[ok]", "[!!]", "[??]"
|
|
21
24
|
|
|
22
25
|
|
|
23
|
-
def _rpc(method, params):
|
|
26
|
+
def _rpc(method: str, params: list[Any]) -> Any:
|
|
24
27
|
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method,
|
|
25
28
|
"params": params}).encode()
|
|
26
|
-
last =
|
|
29
|
+
last: Exception = RuntimeError("no RPC endpoint reachable")
|
|
27
30
|
for rpc in RPCS:
|
|
28
31
|
try:
|
|
29
32
|
req = urllib.request.Request(rpc, data=body, headers={
|
|
@@ -38,13 +41,14 @@ def _rpc(method, params):
|
|
|
38
41
|
raise last
|
|
39
42
|
|
|
40
43
|
|
|
41
|
-
def looks_like_minimal_proxy(bytecode):
|
|
44
|
+
def looks_like_minimal_proxy(bytecode: str | None) -> bool:
|
|
42
45
|
"""ERC-1167 minimal proxies (Polymarket deposit wallets included) are a
|
|
43
46
|
tiny stub; a bare EOA has no code at all."""
|
|
44
|
-
return bytecode not in (None, "", "0x") and len(bytecode) < 400
|
|
47
|
+
return bytecode not in (None, "", "0x") and len(bytecode or "") < 400
|
|
45
48
|
|
|
46
49
|
|
|
47
|
-
def advise_sig_type(funder_is_contract, owner_is_eoa,
|
|
50
|
+
def advise_sig_type(funder_is_contract: bool, owner_is_eoa: bool,
|
|
51
|
+
funder_equals_eoa: bool) -> tuple[int | None, str]:
|
|
48
52
|
"""One sentence of advice from the on-chain facts."""
|
|
49
53
|
if funder_equals_eoa:
|
|
50
54
|
return 0, "funder IS the EOA: signature_type=0"
|
|
@@ -57,12 +61,12 @@ def advise_sig_type(funder_is_contract, owner_is_eoa, funder_equals_eoa):
|
|
|
57
61
|
return None, "funder has no code on-chain: not a deployed wallet"
|
|
58
62
|
|
|
59
63
|
|
|
60
|
-
def check(ok, label, detail=""):
|
|
64
|
+
def check(ok: object, label: str, detail: str = "") -> bool:
|
|
61
65
|
print(f"{GREEN if ok else RED} {label}" + (f": {detail}" if detail else ""))
|
|
62
66
|
return bool(ok)
|
|
63
67
|
|
|
64
68
|
|
|
65
|
-
def main(argv=None):
|
|
69
|
+
def main(argv: list[str] | None = None) -> int:
|
|
66
70
|
argv = sys.argv[1:] if argv is None else argv
|
|
67
71
|
market_arg = None
|
|
68
72
|
if "--market" in argv:
|
|
@@ -109,9 +113,15 @@ def main(argv=None):
|
|
|
109
113
|
eoa = Account.from_key(key).address
|
|
110
114
|
print(f" derived EOA: {eoa}")
|
|
111
115
|
print(f" POLY_FUNDER: {funder or '(unset)'} POLY_SIG_TYPE: {sig}")
|
|
116
|
+
try:
|
|
117
|
+
sig_val = int(sig)
|
|
118
|
+
except ValueError:
|
|
119
|
+
all_ok &= check(False, "POLY_SIG_TYPE is a number",
|
|
120
|
+
f"{sig!r} is not; use 0 (EOA) or 3 (deposit wallet)")
|
|
121
|
+
sig_val = -1
|
|
112
122
|
|
|
113
123
|
# 3. on-chain truth about the funder
|
|
114
|
-
expected_sig = 0
|
|
124
|
+
expected_sig: int | None = 0
|
|
115
125
|
if funder and funder.lower() != eoa.lower():
|
|
116
126
|
try:
|
|
117
127
|
code = _rpc("eth_getCode", [funder, "latest"])
|
|
@@ -123,20 +133,21 @@ def main(argv=None):
|
|
|
123
133
|
owner = "0x" + res[-40:]
|
|
124
134
|
except Exception:
|
|
125
135
|
pass
|
|
126
|
-
owner_is_eoa =
|
|
136
|
+
owner_is_eoa = owner is not None and owner.lower() == eoa.lower()
|
|
127
137
|
expected_sig, advice = advise_sig_type(is_contract, owner_is_eoa, False)
|
|
128
138
|
all_ok &= check(expected_sig is not None, "funder wallet on-chain", advice)
|
|
129
139
|
bal = int(_rpc("eth_call", [{"to": PUSD, "data":
|
|
130
140
|
"0x70a08231" + funder.lower()[2:].rjust(64, "0")}, "latest"]), 16) / 1e6
|
|
131
141
|
print(f" on-chain pUSD at funder: {bal:.2f}")
|
|
132
142
|
except Exception as e:
|
|
133
|
-
check(False, "on-chain funder checks (RPC)", str(e)[:120])
|
|
143
|
+
all_ok &= check(False, "on-chain funder checks (RPC)", str(e)[:120])
|
|
144
|
+
expected_sig = None
|
|
134
145
|
else:
|
|
135
146
|
expected_sig, advice = advise_sig_type(False, False, True)
|
|
136
147
|
check(True, "funder", advice)
|
|
137
148
|
|
|
138
149
|
if expected_sig is not None:
|
|
139
|
-
good =
|
|
150
|
+
good = sig_val == expected_sig
|
|
140
151
|
all_ok &= check(good, f"POLY_SIG_TYPE={sig} matches the wallet type",
|
|
141
152
|
"" if good else f"set POLY_SIG_TYPE={expected_sig}")
|
|
142
153
|
|
|
@@ -149,7 +160,7 @@ def main(argv=None):
|
|
|
149
160
|
all_ok &= seen
|
|
150
161
|
if not seen:
|
|
151
162
|
for st in (0, 1, 2, 3):
|
|
152
|
-
if st ==
|
|
163
|
+
if st == sig_val:
|
|
153
164
|
continue
|
|
154
165
|
try:
|
|
155
166
|
alt = PolymarketExecutor(signature_type=st).collateral()
|