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.
Files changed (41) hide show
  1. {pmquant-0.3.0 → pmquant-0.4.0}/.github/workflows/canary.yml +2 -2
  2. {pmquant-0.3.0 → pmquant-0.4.0}/.github/workflows/publish.yml +2 -2
  3. {pmquant-0.3.0 → pmquant-0.4.0}/.github/workflows/test.yml +5 -4
  4. {pmquant-0.3.0 → pmquant-0.4.0}/.gitignore +1 -0
  5. {pmquant-0.3.0 → pmquant-0.4.0}/CHANGELOG.md +24 -0
  6. {pmquant-0.3.0 → pmquant-0.4.0}/PKG-INFO +11 -1
  7. {pmquant-0.3.0 → pmquant-0.4.0}/README.md +2 -0
  8. {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/dash/bot_dash.py +4 -4
  9. {pmquant-0.3.0 → pmquant-0.4.0}/pyproject.toml +18 -2
  10. {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/__init__.py +2 -2
  11. {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/data.py +70 -34
  12. {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/doctor.py +23 -12
  13. {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/executor.py +50 -31
  14. {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/mcp.py +20 -14
  15. pmquant-0.4.0/src/pmq/py.typed +0 -0
  16. {pmquant-0.3.0 → pmquant-0.4.0}/tests/test_data.py +82 -0
  17. pmquant-0.4.0/tests/test_doctor.py +160 -0
  18. {pmquant-0.3.0 → pmquant-0.4.0}/tests/test_executor.py +101 -0
  19. pmquant-0.4.0/tests/test_mcp.py +133 -0
  20. pmquant-0.3.0/tests/test_doctor.py +0 -26
  21. pmquant-0.3.0/tests/test_mcp.py +0 -38
  22. {pmquant-0.3.0 → pmquant-0.4.0}/AGENTS.md +0 -0
  23. {pmquant-0.3.0 → pmquant-0.4.0}/CLAUDE.md +0 -0
  24. {pmquant-0.3.0 → pmquant-0.4.0}/CONTRIBUTING.md +0 -0
  25. {pmquant-0.3.0 → pmquant-0.4.0}/LICENSE +0 -0
  26. {pmquant-0.3.0 → pmquant-0.4.0}/SECURITY.md +0 -0
  27. {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/README.md +0 -0
  28. {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/bot.py +0 -0
  29. {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/dash/dash.html +0 -0
  30. {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/pmq-bot.service +0 -0
  31. {pmquant-0.3.0 → pmquant-0.4.0}/bot-template/strategy.py +0 -0
  32. {pmquant-0.3.0 → pmquant-0.4.0}/docs/assets/pmq-doctor.svg +0 -0
  33. {pmquant-0.3.0 → pmquant-0.4.0}/docs/recipes.md +0 -0
  34. {pmquant-0.3.0 → pmquant-0.4.0}/docs/rounding-study.md +0 -0
  35. {pmquant-0.3.0 → pmquant-0.4.0}/docs/war-story.md +0 -0
  36. {pmquant-0.3.0 → pmquant-0.4.0}/examples/fak_buy_guarded.py +0 -0
  37. {pmquant-0.3.0 → pmquant-0.4.0}/examples/read_market.py +0 -0
  38. {pmquant-0.3.0 → pmquant-0.4.0}/llms.txt +0 -0
  39. {pmquant-0.3.0 → pmquant-0.4.0}/src/pmq/exceptions.py +0 -0
  40. {pmquant-0.3.0 → pmquant-0.4.0}/tests/test_canary_live.py +0 -0
  41. {pmquant-0.3.0 → pmquant-0.4.0}/tests/test_template_engine.py +0 -0
@@ -12,8 +12,8 @@ jobs:
12
12
  canary:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@v4
16
- - uses: actions/setup-python@v5
15
+ - uses: actions/checkout@v5
16
+ - uses: actions/setup-python@v6
17
17
  with:
18
18
  python-version: "3.12"
19
19
  - run: pip install -e ".[dev]"
@@ -11,10 +11,10 @@ jobs:
11
11
  permissions:
12
12
  id-token: write
13
13
  steps:
14
- - uses: actions/checkout@v4
14
+ - uses: actions/checkout@v5
15
15
 
16
16
  - name: Set up Python
17
- uses: actions/setup-python@v5
17
+ uses: actions/setup-python@v6
18
18
  with:
19
19
  python-version: "3.12"
20
20
 
@@ -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@v4
13
- - uses: actions/setup-python@v5
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: pytest -q
18
+ - run: mypy
19
+ - run: pytest -q --cov=pmq --cov-fail-under=85
@@ -6,3 +6,4 @@ build/
6
6
  .venv/
7
7
  .pytest_cache/
8
8
  .env
9
+ .coverage
@@ -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.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
  [![PyPI](https://img.shields.io/pypi/v/pmquant)](https://pypi.org/project/pmquant/)
30
38
  [![tests](https://github.com/crp4222/pmq/actions/workflows/test.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/test.yml)
31
39
  [![canary](https://github.com/crp4222/pmq/actions/workflows/canary.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
40
+ [![coverage gate](https://img.shields.io/badge/coverage-%E2%89%A585%25%20enforced%20in%20CI-blue)](.github/workflows/test.yml)
41
+ [![typed](https://img.shields.io/badge/types-mypy%20strict-blue)](pyproject.toml)
32
42
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
33
43
 
34
44
  Fail-closed execution and market data for **Polymarket CLOB V2**, in Python.
@@ -3,6 +3,8 @@
3
3
  [![PyPI](https://img.shields.io/pypi/v/pmquant)](https://pypi.org/project/pmquant/)
4
4
  [![tests](https://github.com/crp4222/pmq/actions/workflows/test.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/test.yml)
5
5
  [![canary](https://github.com/crp4222/pmq/actions/workflows/canary.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
6
+ [![coverage gate](https://img.shields.io/badge/coverage-%E2%89%A585%25%20enforced%20in%20CI-blue)](.github/workflows/test.yml)
7
+ [![typed](https://img.shields.io/badge/types-mypy%20strict-blue)](pyproject.toml)
6
8
  [![license](https://img.shields.io/badge/license-MIT-green)](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 favbot journal;
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
- """favbot systemd state + last collateral line from its journal."""
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 = "favbot-dash"
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"favbot-dash listening on :{PORT}", flush=True)
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.3.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.3.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
- def fee(price, shares, rate=FEE_RATES["crypto"]):
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, logger=None):
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
- return data[0]
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
- return ev[0]["markets"][0]
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, outcome_a=None, outcome_b=None):
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.get("outcomes")
108
- token_ids = json.loads(m["clobTokenIds"]) if isinstance(m.get("clobTokenIds"), str) else m.get("clobTokenIds")
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
- "condition_id": m.get("conditionId"),
113
- "slug": m.get("slug"),
114
- "token_a": token_ids[a],
115
- "token_b": token_ids[b],
116
- "outcome_a": outcomes[a],
117
- "outcome_b": outcomes[b],
118
- "outcome_prices_raw": m.get("outcomePrices"),
119
- "idx_a": a,
120
- "end_ts": _end_ts(m),
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
- return http_get_json(f"{CLOB}/book?token_id={token_id}", logger=logger)
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
- def _f(k):
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, threshold=0.90):
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, limit=200):
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, logger=None):
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 = None
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, funder_equals_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 = bool(owner) and owner.lower() == eoa.lower()
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 = int(sig) == expected_sig
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 == int(sig):
163
+ if st == sig_val:
153
164
  continue
154
165
  try:
155
166
  alt = PolymarketExecutor(signature_type=st).collateral()