pmquant 0.2.0__tar.gz → 0.3.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 (38) hide show
  1. {pmquant-0.2.0 → pmquant-0.3.0}/.github/workflows/publish.yml +11 -0
  2. {pmquant-0.2.0 → pmquant-0.3.0}/CHANGELOG.md +16 -0
  3. pmquant-0.3.0/CLAUDE.md +52 -0
  4. pmquant-0.3.0/CONTRIBUTING.md +52 -0
  5. {pmquant-0.2.0 → pmquant-0.3.0}/PKG-INFO +19 -1
  6. {pmquant-0.2.0 → pmquant-0.3.0}/README.md +18 -0
  7. pmquant-0.3.0/docs/assets/pmq-doctor.svg +17 -0
  8. pmquant-0.3.0/docs/recipes.md +93 -0
  9. {pmquant-0.2.0 → pmquant-0.3.0}/docs/rounding-study.md +11 -4
  10. {pmquant-0.2.0 → pmquant-0.3.0}/pyproject.toml +2 -1
  11. {pmquant-0.2.0 → pmquant-0.3.0}/src/pmq/__init__.py +1 -1
  12. pmquant-0.3.0/src/pmq/doctor.py +191 -0
  13. {pmquant-0.2.0 → pmquant-0.3.0}/tests/test_data.py +11 -0
  14. pmquant-0.3.0/tests/test_doctor.py +26 -0
  15. {pmquant-0.2.0 → pmquant-0.3.0}/.github/workflows/canary.yml +0 -0
  16. {pmquant-0.2.0 → pmquant-0.3.0}/.github/workflows/test.yml +0 -0
  17. {pmquant-0.2.0 → pmquant-0.3.0}/.gitignore +0 -0
  18. {pmquant-0.2.0 → pmquant-0.3.0}/AGENTS.md +0 -0
  19. {pmquant-0.2.0 → pmquant-0.3.0}/LICENSE +0 -0
  20. {pmquant-0.2.0 → pmquant-0.3.0}/SECURITY.md +0 -0
  21. {pmquant-0.2.0 → pmquant-0.3.0}/bot-template/README.md +0 -0
  22. {pmquant-0.2.0 → pmquant-0.3.0}/bot-template/bot.py +0 -0
  23. {pmquant-0.2.0 → pmquant-0.3.0}/bot-template/dash/bot_dash.py +0 -0
  24. {pmquant-0.2.0 → pmquant-0.3.0}/bot-template/dash/dash.html +0 -0
  25. {pmquant-0.2.0 → pmquant-0.3.0}/bot-template/pmq-bot.service +0 -0
  26. {pmquant-0.2.0 → pmquant-0.3.0}/bot-template/strategy.py +0 -0
  27. {pmquant-0.2.0 → pmquant-0.3.0}/docs/war-story.md +0 -0
  28. {pmquant-0.2.0 → pmquant-0.3.0}/examples/fak_buy_guarded.py +0 -0
  29. {pmquant-0.2.0 → pmquant-0.3.0}/examples/read_market.py +0 -0
  30. {pmquant-0.2.0 → pmquant-0.3.0}/llms.txt +0 -0
  31. {pmquant-0.2.0 → pmquant-0.3.0}/src/pmq/data.py +0 -0
  32. {pmquant-0.2.0 → pmquant-0.3.0}/src/pmq/exceptions.py +0 -0
  33. {pmquant-0.2.0 → pmquant-0.3.0}/src/pmq/executor.py +0 -0
  34. {pmquant-0.2.0 → pmquant-0.3.0}/src/pmq/mcp.py +0 -0
  35. {pmquant-0.2.0 → pmquant-0.3.0}/tests/test_canary_live.py +0 -0
  36. {pmquant-0.2.0 → pmquant-0.3.0}/tests/test_executor.py +0 -0
  37. {pmquant-0.2.0 → pmquant-0.3.0}/tests/test_mcp.py +0 -0
  38. {pmquant-0.2.0 → pmquant-0.3.0}/tests/test_template_engine.py +0 -0
@@ -18,6 +18,17 @@ jobs:
18
18
  with:
19
19
  python-version: "3.12"
20
20
 
21
+ - name: Tag matches pyproject version
22
+ if: github.event_name == 'release'
23
+ run: |
24
+ pkg=$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)
25
+ init=$(grep -m1 '^__version__' src/pmq/__init__.py | cut -d'"' -f2)
26
+ tag="${GITHUB_REF_NAME#v}"
27
+ if [ "$pkg" != "$tag" ] || [ "$init" != "$tag" ]; then
28
+ echo "tag v$tag, pyproject $pkg, __init__ $init: bump all three before releasing"
29
+ exit 1
30
+ fi
31
+
21
32
  - name: Install
22
33
  run: pip install -e ".[dev]"
23
34
 
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 (2026-07-03)
4
+
5
+ * New: `pmq-doctor`, a read-only diagnosis command for Polymarket V2 setups.
6
+ Checks the installed py-clob-client-v2 against the verified surface,
7
+ derives the EOA from `POLY_PRIVATE_KEY` (never printed), reads the funder
8
+ on-chain (contract vs EOA, `owner()`), advises the right `POLY_SIG_TYPE`,
9
+ verifies the CLOB sees collateral with the configured identity (and probes
10
+ the other signature types when it does not), and optionally checks one
11
+ market (`--market <slug>`: book, min_order_size, tick, taker fee).
12
+ * Docs: docs/recipes.md cookbook (trade a market, paper-test a strategy,
13
+ read positions, verify builder attribution on-chain), demo card in the
14
+ README.
15
+ * CI: the publish workflow refuses to upload when the release tag does not
16
+ match the version in pyproject.toml (the failure mode that blocked the
17
+ first 0.3.0 release).
18
+
3
19
  ## 0.2.0 (2026-07-03)
4
20
 
5
21
  * New: `positions(user)` and `event_markets(slug)` in the data layer; `event`
@@ -0,0 +1,52 @@
1
+ # pmq: engineering invariants for agents CONTRIBUTING to this repo
2
+
3
+ (AGENTS.md in this repo is for agents USING the library; this file is for
4
+ agents EDITING it. Read both before changing code.)
5
+
6
+ ## Never weaken (the product IS these properties)
7
+
8
+ 1. **The fail-closed fill contract**: a `Fill` books only what the exchange
9
+ confirmed (`orderID` + `success is not False` + matched amounts); 4xx is a
10
+ clean rejection; timeout/5xx raises `OrderUncertain`; unparseable = zero.
11
+ Any change that books more optimistically is a regression by definition,
12
+ whatever it fixes elsewhere. `reconcile()` must keep meaning cancel +
13
+ `get_trades` truth.
14
+ 2. **Startup introspection** (`_EXPECTED_METHODS`/`_EXPECTED_MARKET_ARGS`):
15
+ the executor REFUSES to run on a drifted py-clob-client-v2. When bumping
16
+ the client dependency, re-verify signatures by introspection and update
17
+ the tables in the same commit.
18
+ 3. **Builder code policy**: default = maintainer's code, DISCLOSED in README
19
+ and code comment, opt-out one line (`builder_code=None` / env). Never
20
+ hide it, never remove the disclosure, never make opt-out harder. This is
21
+ the trust model (JKorf pattern).
22
+ 4. **No strategy content, ever**: the maintainer's private bot strategy
23
+ (bands, timing, hours, families, sizing) must never appear in code, docs,
24
+ tests, commits or issues. The bot-template ships deliberately naive
25
+ demos only.
26
+ 5. **Claims must be falsifiable**: no superlatives in README/docs; dated
27
+ claims with evidence (comparison table, on-chain receipts, measured
28
+ studies). If you cannot prove it, do not write it.
29
+ 6. **MCP safety gates**: trading tools are REGISTERED only when the operator
30
+ sets `PMQ_MCP_LIVE=1`; per-order `PMQ_MCP_MAX_USD` cap enforced before
31
+ any client call. Read tools must keep working with zero credentials.
32
+
33
+ ## Working rules
34
+
35
+ * Tests green (`pytest -q`) and `ruff check .` clean before any push; add
36
+ tests with every behavior change. Network-touching tests go to
37
+ `tests/test_canary_live.py` behind `PMQ_CANARY=1`, never in default CI.
38
+ * Exchange rules (min size, tick, fee rate) are READ from the venue
39
+ (`book_meta`, `fee_rate`), not hardcoded. `FEE_RATES` is a documented
40
+ snapshot of the official schedule used for estimates.
41
+ * Releases: bump version in `pyproject.toml` AND `src/pmq/__init__.py`,
42
+ update CHANGELOG.md, push, then `gh release create vX.Y.Z`: PyPI publish
43
+ is automatic via trusted publishing (no tokens anywhere). PyPI name is
44
+ `pmquant`, import name `pmq`: keep the README line explaining it.
45
+ * The weekly canary workflow is the drift alarm: if it opens an issue, the
46
+ fix starts by re-running the introspection against the new surface, not
47
+ by loosening the checks.
48
+ * Keep the library small and auditable (five modules): resist adding
49
+ dependencies; stdlib first. Anything bot-shaped belongs in bot-template/,
50
+ not in the package.
51
+ * Style: no em-dashes and no " - " connectors anywhere (strong user rule);
52
+ keep comments sparse and constraint-focused.
@@ -0,0 +1,52 @@
1
+ # pmq: engineering invariants for agents CONTRIBUTING to this repo
2
+
3
+ (AGENTS.md in this repo is for agents USING the library; this file is for
4
+ agents EDITING it. Read both before changing code.)
5
+
6
+ ## Never weaken (the product IS these properties)
7
+
8
+ 1. **The fail-closed fill contract**: a `Fill` books only what the exchange
9
+ confirmed (`orderID` + `success is not False` + matched amounts); 4xx is a
10
+ clean rejection; timeout/5xx raises `OrderUncertain`; unparseable = zero.
11
+ Any change that books more optimistically is a regression by definition,
12
+ whatever it fixes elsewhere. `reconcile()` must keep meaning cancel +
13
+ `get_trades` truth.
14
+ 2. **Startup introspection** (`_EXPECTED_METHODS`/`_EXPECTED_MARKET_ARGS`):
15
+ the executor REFUSES to run on a drifted py-clob-client-v2. When bumping
16
+ the client dependency, re-verify signatures by introspection and update
17
+ the tables in the same commit.
18
+ 3. **Builder code policy**: default = maintainer's code, DISCLOSED in README
19
+ and code comment, opt-out one line (`builder_code=None` / env). Never
20
+ hide it, never remove the disclosure, never make opt-out harder. This is
21
+ the trust model (JKorf pattern).
22
+ 4. **No strategy content, ever**: the maintainer's private bot strategy
23
+ (bands, timing, hours, families, sizing) must never appear in code, docs,
24
+ tests, commits or issues. The bot-template ships deliberately naive
25
+ demos only.
26
+ 5. **Claims must be falsifiable**: no superlatives in README/docs; dated
27
+ claims with evidence (comparison table, on-chain receipts, measured
28
+ studies). If you cannot prove it, do not write it.
29
+ 6. **MCP safety gates**: trading tools are REGISTERED only when the operator
30
+ sets `PMQ_MCP_LIVE=1`; per-order `PMQ_MCP_MAX_USD` cap enforced before
31
+ any client call. Read tools must keep working with zero credentials.
32
+
33
+ ## Working rules
34
+
35
+ * Tests green (`pytest -q`) and `ruff check .` clean before any push; add
36
+ tests with every behavior change. Network-touching tests go to
37
+ `tests/test_canary_live.py` behind `PMQ_CANARY=1`, never in default CI.
38
+ * Exchange rules (min size, tick, fee rate) are READ from the venue
39
+ (`book_meta`, `fee_rate`), not hardcoded. `FEE_RATES` is a documented
40
+ snapshot of the official schedule used for estimates.
41
+ * Releases: bump version in `pyproject.toml` AND `src/pmq/__init__.py`,
42
+ update CHANGELOG.md, push, then `gh release create vX.Y.Z`: PyPI publish
43
+ is automatic via trusted publishing (no tokens anywhere). PyPI name is
44
+ `pmquant`, import name `pmq`: keep the README line explaining it.
45
+ * The weekly canary workflow is the drift alarm: if it opens an issue, the
46
+ fix starts by re-running the introspection against the new surface, not
47
+ by loosening the checks.
48
+ * Keep the library small and auditable (five modules): resist adding
49
+ dependencies; stdlib first. Anything bot-shaped belongs in bot-template/,
50
+ not in the package.
51
+ * Style: no em-dashes and no " - " connectors anywhere (strong user rule);
52
+ keep comments sparse and constraint-focused.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pmquant
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -80,6 +80,24 @@ settled, with the builder code visible in the calldata. Additionally, a weekly
80
80
  and the installed client surface, and opens an issue by itself if Polymarket
81
81
  drifts.
82
82
 
83
+ ## pmq-doctor: diagnose your setup in one command
84
+
85
+ ```bash
86
+ pip install pmquant && pmq-doctor --market <slug>
87
+ ```
88
+
89
+ It checks, in order: the installed client surface (introspection), your
90
+ derived EOA, the funder wallet on-chain (`owner()` and bytecode: is it a
91
+ deposit wallet?), whether `POLY_SIG_TYPE` matches the wallet type, whether
92
+ the CLOB actually sees your collateral (and if not, WHICH sig_type does),
93
+ and the target market's minimum size and tick. Real output on a real
94
+ deposit-wallet account:
95
+
96
+ ![pmq-doctor output](docs/assets/pmq-doctor.svg)
97
+
98
+ If you landed here from "the order signer address has to be the address of
99
+ the API KEY" or a CLOB balance of 0 with funds on-chain: this is the tool.
100
+
83
101
  ## The contract: nothing is booked without exchange confirmation
84
102
 
85
103
  | Situation | What pmq does |
@@ -54,6 +54,24 @@ settled, with the builder code visible in the calldata. Additionally, a weekly
54
54
  and the installed client surface, and opens an issue by itself if Polymarket
55
55
  drifts.
56
56
 
57
+ ## pmq-doctor: diagnose your setup in one command
58
+
59
+ ```bash
60
+ pip install pmquant && pmq-doctor --market <slug>
61
+ ```
62
+
63
+ It checks, in order: the installed client surface (introspection), your
64
+ derived EOA, the funder wallet on-chain (`owner()` and bytecode: is it a
65
+ deposit wallet?), whether `POLY_SIG_TYPE` matches the wallet type, whether
66
+ the CLOB actually sees your collateral (and if not, WHICH sig_type does),
67
+ and the target market's minimum size and tick. Real output on a real
68
+ deposit-wallet account:
69
+
70
+ ![pmq-doctor output](docs/assets/pmq-doctor.svg)
71
+
72
+ If you landed here from "the order signer address has to be the address of
73
+ the API KEY" or a CLOB balance of 0 with funds on-chain: this is the tool.
74
+
57
75
  ## The contract: nothing is booked without exchange confirmation
58
76
 
59
77
  | Situation | What pmq does |
@@ -0,0 +1,17 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="880" height="292" font-family="SFMono-Regular,Consolas,Menlo,monospace" font-size="12.5">
2
+ <rect width="880" height="292" rx="10" fill="#16161a"/>
3
+ <circle cx="20" cy="20" r="5.5" fill="#ff5f57"/><circle cx="38" cy="20" r="5.5" fill="#febc2e"/><circle cx="56" cy="20" r="5.5" fill="#28c840"/>
4
+ <text x="440.0" y="24" fill="#71717a" text-anchor="middle" font-size="11">pmq-doctor · real account, real output</text>
5
+ <text x="18" y="54" fill="#ffffff" xml:space="preserve">pmq-doctor: Polymarket V2 setup diagnosis (read-only, no key ever printed)</text>
6
+ <text x="18" y="73" fill="#22c55e" xml:space="preserve">[ok] installed py-clob-client-v2 matches the verified surface</text>
7
+ <text x="18" y="92" fill="#22c55e" xml:space="preserve">[ok] POLY_PRIVATE_KEY present (never printed)</text>
8
+ <text x="18" y="111" fill="#c3c2b7" xml:space="preserve"> derived EOA: 0xdb581D8BE7b5C53C6D832083a8A49D0CBF73d6e1</text>
9
+ <text x="18" y="130" fill="#c3c2b7" xml:space="preserve"> POLY_FUNDER: 0x76cd962fc8c5f5e5a0cbe14c74339aa78268da58 POLY_SIG_TYPE: 3</text>
10
+ <text x="18" y="149" fill="#22c55e" xml:space="preserve">[ok] funder wallet on-chain: funder is a contract owned by your EOA: a deposit wallet, signature_type=3 (POLY_1271)</text>
11
+ <text x="18" y="168" fill="#c3c2b7" xml:space="preserve"> on-chain pUSD at funder: 39.42</text>
12
+ <text x="18" y="187" fill="#22c55e" xml:space="preserve">[ok] POLY_SIG_TYPE=3 matches the wallet type</text>
13
+ <text x="18" y="206" fill="#22c55e" xml:space="preserve">[ok] CLOB sees collateral with sig_type=3: 39.42 USDC</text>
14
+ <text x="18" y="225" fill="#22c55e" xml:space="preserve">[ok] market btc-updown-15m-1783081800: bid=0.48 ask=0.49 min_order_size=5.0 tick=0.01</text>
15
+ <text x="18" y="244" fill="#c3c2b7" xml:space="preserve"> taker fee at ask: 0.0175$/share (crypto table; authoritative per-market rate via executor.fee_rate)</text>
16
+ <text x="18" y="263" fill="#eab308" xml:space="preserve">verdict: everything green, orders should work</text>
17
+ </svg>
@@ -0,0 +1,93 @@
1
+ # Recipes
2
+
3
+ Copy-paste starting points. Everything below is Python against `pip install
4
+ pmquant` (`import pmq`); execution snippets read `POLY_PRIVATE_KEY`,
5
+ `POLY_FUNDER`, `POLY_SIG_TYPE` from the environment and never print them.
6
+
7
+ ## Trade a political market in 20 lines
8
+
9
+ ```python
10
+ import pmq
11
+
12
+ # discover: any gamma slug works (politics, sports, crypto)
13
+ pm = pmq.parse_market(pmq.get_market("mississippi-gubernatorial-election-presley-d-vs-reeves-r"))
14
+ book = pmq.get_book(pm["token_a"]) # real-time, trustable
15
+ bid, bid_sz, ask, ask_sz = pmq.best_bid_ask(book)
16
+ rules = pmq.book_meta(book) # min_order_size, tick_size
17
+
18
+ ex = pmq.PolymarketExecutor() # sig_type 3 = app wallet
19
+ ex.require_collateral(10)
20
+ if ask and ask * rules["min_order_size"] <= 10:
21
+ fill = ex.buy_fak(pm["token_a"], price_cap=ask, usd=10.0)
22
+ if fill: # book ONLY what matched
23
+ print(f"bought {fill.matched_shares} {pm['outcome_a']} at {fill.price:.3f}")
24
+ ```
25
+
26
+ ## Scan a multi-outcome event (election, tournament)
27
+
28
+ ```python
29
+ import pmq
30
+
31
+ total_asks = 0.0
32
+ for pm in pmq.event_markets("world-cup-winner"):
33
+ _, _, ask, _ = pmq.best_bid_ask(pmq.get_book(pm["token_a"]))
34
+ if ask is None:
35
+ print(f"{pm['outcome_a']:24s} unquoted (basket incomplete)")
36
+ continue
37
+ total_asks += ask
38
+ print(f"{pm['outcome_a']:24s} ask {ask}")
39
+ print("sum of asks:", round(total_asks, 4), "(a full basket below 1 - fees pays 1)")
40
+ ```
41
+
42
+ ## What do I hold?
43
+
44
+ ```python
45
+ import os
46
+ import pmq
47
+
48
+ for p in pmq.positions(os.environ["POLY_FUNDER"]):
49
+ print(p["slug"], p["outcome"], p["size"], "avg", p["avgPrice"],
50
+ "now", p.get("curPrice"), "value", p.get("currentValue"))
51
+ ```
52
+
53
+ ## Paper-test a strategy on any market
54
+
55
+ Use [bot-template/](../bot-template/): implement `watchlist()` and
56
+ `decide()` in `strategy.py`, run `python bot.py 24`, read
57
+ `bot_runs/windows.csv`. Paper fills execute against the REAL ask, capped by
58
+ displayed size and the per-market exchange minimum, and are scored with the
59
+ real fee at resolution: if it does not survive paper, it will not survive
60
+ live.
61
+
62
+ ## Budget with the real fee
63
+
64
+ ```python
65
+ import pmq
66
+
67
+ ex = pmq.PolymarketExecutor()
68
+ rate = ex.fee_rate(pm["condition_id"]) # authoritative, from the exchange
69
+ cost_per_share = ask + pmq.fee(ask, 1.0, rate)
70
+ shares_affordable = budget_usd / cost_per_share
71
+ ```
72
+
73
+ ## Verify builder attribution on-chain
74
+
75
+ ```python
76
+ import pmq
77
+
78
+ sh, usd, fees = ex.trades_totals(pm["condition_id"]) # your fills, exchange truth
79
+ # every matched order settles on the CTF Exchange V2; the bytes32 builder
80
+ # code rides in the calldata. Grep any of your settlement transactions:
81
+ # https://polygonscan.com/tx/<transactionHash from get_trades>
82
+ # and search the input data for your builder code (without the 0x prefix).
83
+ ```
84
+
85
+ ## After a timeout or 5xx
86
+
87
+ ```python
88
+ try:
89
+ fill = ex.buy_fak(token, cap, usd)
90
+ except pmq.OrderUncertain:
91
+ sh, usd_spent, fees = ex.reconcile(pm["condition_id"], token)
92
+ # book sh/usd_spent, nothing else, and only now place new orders
93
+ ```
@@ -36,10 +36,17 @@ amounts against requested size, cross-checked with `get_trades`.
36
36
  unrounded size or price, it diverges from what the exchange signed. Book
37
37
  from the response's matched amounts (what pmq's `Fill` does), never from
38
38
  your request.
39
- 3. Limit-path fills never exceeded the signed size in these tests. The
40
- overfill reports (filled 5.051 on a size-5 order) are consistent with the
41
- MARKET-order path instead, where the contract is "spend this amount" and
42
- the share count is the division remainder of amount by price.
39
+ 3. Limit-path fills never exceeded the signed size in these tests because
40
+ they matched AT the limit price. The mechanism behind the overfill
41
+ reports (credit to gmoutsin in
42
+ [#89](https://github.com/Polymarket/py-clob-client-v2/issues/89)): the
43
+ signed V2 order is an amounts PAIR (makerAmount USDC, takerAmount
44
+ tokens), a ratio with a worst-case bound, not a (price, size) tuple.
45
+ Under price improvement the engine preserves your dollars and returns
46
+ proportionally MORE tokens (4.95 at a 0.98 ask = 5.051 tokens on a
47
+ "size 5" order at 0.99). Strictly favorable, but it breaks size-based
48
+ accounting: book from the matched amounts, and never rely on a
49
+ marketable limit to cap token count exactly.
43
50
  4. The per-market minimum size is enforced server-side with a clean 400, and
44
51
  is readable in advance from the book response (`min_order_size`, exposed
45
52
  by `pmq.book_meta`).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pmquant"
7
- version = "0.2.0"
7
+ version = "0.3.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" }
@@ -35,6 +35,7 @@ dev = ["pytest>=8", "mcp>=1.2", "ruff>=0.6"]
35
35
 
36
36
  [project.scripts]
37
37
  pmq-mcp = "pmq.mcp:main"
38
+ pmq-doctor = "pmq.doctor:main"
38
39
 
39
40
  [tool.hatch.build.targets.wheel]
40
41
  packages = ["src/pmq"]
@@ -25,7 +25,7 @@ from .data import (
25
25
  )
26
26
  from .exceptions import IntrospectionMismatch, OrderUncertain, PmqError
27
27
 
28
- __version__ = "0.2.0"
28
+ __version__ = "0.3.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",
@@ -0,0 +1,191 @@
1
+ """pmq-doctor: one command that diagnoses the classic Polymarket V2 setup
2
+ failures (the ones behind a dozen open issues on the official client):
3
+ wrong signature_type for a deposit wallet, api key confusion, CLOB balance 0
4
+ while funds sit on-chain, drifted client surface, per-market minimums.
5
+
6
+ Read-only: derives addresses, calls public RPC and CLOB endpoints. Never
7
+ prints or transmits the private key. Exit code 0 when everything is green.
8
+ """
9
+ import inspect
10
+ import json
11
+ import os
12
+ import sys
13
+ import urllib.request
14
+
15
+ from .data import best_bid_ask, book_meta, fee, get_book, get_market, parse_market
16
+
17
+ PUSD = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
18
+ RPCS = ["https://polygon-rpc.com", "https://polygon-bor-rpc.publicnode.com",
19
+ "https://1rpc.io/matic", "https://polygon.drpc.org"]
20
+ GREEN, RED, WARN = "[ok]", "[!!]", "[??]"
21
+
22
+
23
+ def _rpc(method, params):
24
+ body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method,
25
+ "params": params}).encode()
26
+ last = None
27
+ for rpc in RPCS:
28
+ try:
29
+ req = urllib.request.Request(rpc, data=body, headers={
30
+ "Content-Type": "application/json", "User-Agent": "Mozilla/5.0"})
31
+ with urllib.request.urlopen(req, timeout=12) as r:
32
+ out = json.loads(r.read().decode())
33
+ if "error" in out:
34
+ raise RuntimeError(out["error"])
35
+ return out["result"]
36
+ except Exception as e:
37
+ last = e
38
+ raise last
39
+
40
+
41
+ def looks_like_minimal_proxy(bytecode):
42
+ """ERC-1167 minimal proxies (Polymarket deposit wallets included) are a
43
+ tiny stub; a bare EOA has no code at all."""
44
+ return bytecode not in (None, "", "0x") and len(bytecode) < 400
45
+
46
+
47
+ def advise_sig_type(funder_is_contract, owner_is_eoa, funder_equals_eoa):
48
+ """One sentence of advice from the on-chain facts."""
49
+ if funder_equals_eoa:
50
+ return 0, "funder IS the EOA: signature_type=0"
51
+ if funder_is_contract and owner_is_eoa:
52
+ return 3, ("funder is a contract owned by your EOA: a deposit wallet, "
53
+ "signature_type=3 (POLY_1271)")
54
+ if funder_is_contract:
55
+ return None, ("funder is a contract NOT owned by this EOA: wrong "
56
+ "POLY_PRIVATE_KEY, or someone else's wallet")
57
+ return None, "funder has no code on-chain: not a deployed wallet"
58
+
59
+
60
+ def check(ok, label, detail=""):
61
+ print(f"{GREEN if ok else RED} {label}" + (f": {detail}" if detail else ""))
62
+ return bool(ok)
63
+
64
+
65
+ def main(argv=None):
66
+ argv = sys.argv[1:] if argv is None else argv
67
+ market_arg = None
68
+ if "--market" in argv:
69
+ i = argv.index("--market")
70
+ if i + 1 >= len(argv):
71
+ print("usage: pmq-doctor [--market <gamma-slug>]")
72
+ return 2
73
+ market_arg = argv[i + 1]
74
+ all_ok = True
75
+ print("pmq-doctor: Polymarket V2 setup diagnosis (read-only, no key ever printed)\n")
76
+
77
+ # 1. installed surface
78
+ try:
79
+ from py_clob_client_v2.client import ClobClient
80
+ from py_clob_client_v2.clob_types import MarketOrderArgsV2, OrderType
81
+
82
+ from .executor import _EXPECTED_MARKET_ARGS, _EXPECTED_METHODS
83
+ drifts = []
84
+ for name, params in _EXPECTED_METHODS.items():
85
+ fn = getattr(ClobClient, name, None)
86
+ have = set(inspect.signature(fn).parameters) if fn else set()
87
+ drifts += [f"{name}.{p}" for p in params if p not in have]
88
+ have = set(inspect.signature(MarketOrderArgsV2).parameters)
89
+ drifts += [f"MarketOrderArgsV2.{p}" for p in _EXPECTED_MARKET_ARGS if p not in have]
90
+ if not hasattr(OrderType, "FAK"):
91
+ drifts.append("OrderType.FAK")
92
+ all_ok &= check(not drifts, "installed py-clob-client-v2 matches the verified surface",
93
+ "drifted: " + ", ".join(drifts) if drifts else "")
94
+ except ImportError as e:
95
+ all_ok &= check(False, "py-clob-client-v2 installed", str(e))
96
+ print("\nverdict: pip install py-clob-client-v2")
97
+ return 1
98
+
99
+ # 2. environment and identities
100
+ key = os.environ.get("POLY_PRIVATE_KEY")
101
+ funder = os.environ.get("POLY_FUNDER")
102
+ sig = os.environ.get("POLY_SIG_TYPE", "0")
103
+ if not key:
104
+ check(False, "POLY_PRIVATE_KEY present in the environment")
105
+ print("\nverdict: export POLY_PRIVATE_KEY (data-layer usage needs no key)")
106
+ return 1
107
+ check(True, "POLY_PRIVATE_KEY present (never printed)")
108
+ from eth_account import Account
109
+ eoa = Account.from_key(key).address
110
+ print(f" derived EOA: {eoa}")
111
+ print(f" POLY_FUNDER: {funder or '(unset)'} POLY_SIG_TYPE: {sig}")
112
+
113
+ # 3. on-chain truth about the funder
114
+ expected_sig = 0
115
+ if funder and funder.lower() != eoa.lower():
116
+ try:
117
+ code = _rpc("eth_getCode", [funder, "latest"])
118
+ is_contract = code not in (None, "", "0x")
119
+ owner = None
120
+ try:
121
+ res = _rpc("eth_call", [{"to": funder, "data": "0x8da5cb5b"}, "latest"])
122
+ if isinstance(res, str) and len(res) >= 42:
123
+ owner = "0x" + res[-40:]
124
+ except Exception:
125
+ pass
126
+ owner_is_eoa = bool(owner) and owner.lower() == eoa.lower()
127
+ expected_sig, advice = advise_sig_type(is_contract, owner_is_eoa, False)
128
+ all_ok &= check(expected_sig is not None, "funder wallet on-chain", advice)
129
+ bal = int(_rpc("eth_call", [{"to": PUSD, "data":
130
+ "0x70a08231" + funder.lower()[2:].rjust(64, "0")}, "latest"]), 16) / 1e6
131
+ print(f" on-chain pUSD at funder: {bal:.2f}")
132
+ except Exception as e:
133
+ check(False, "on-chain funder checks (RPC)", str(e)[:120])
134
+ else:
135
+ expected_sig, advice = advise_sig_type(False, False, True)
136
+ check(True, "funder", advice)
137
+
138
+ if expected_sig is not None:
139
+ good = int(sig) == expected_sig
140
+ all_ok &= check(good, f"POLY_SIG_TYPE={sig} matches the wallet type",
141
+ "" if good else f"set POLY_SIG_TYPE={expected_sig}")
142
+
143
+ # 4. CLOB view with the configured identity
144
+ try:
145
+ from .executor import PolymarketExecutor
146
+ ex = PolymarketExecutor()
147
+ usdc = ex.collateral()
148
+ seen = check(usdc > 0, f"CLOB sees collateral with sig_type={sig}", f"{usdc:.2f} USDC")
149
+ all_ok &= seen
150
+ if not seen:
151
+ for st in (0, 1, 2, 3):
152
+ if st == int(sig):
153
+ continue
154
+ try:
155
+ alt = PolymarketExecutor(signature_type=st).collateral()
156
+ if alt > 0:
157
+ print(f" but sig_type={st} sees {alt:.2f} USDC: "
158
+ f"set POLY_SIG_TYPE={st}")
159
+ break
160
+ except Exception:
161
+ continue
162
+ except Exception as e:
163
+ all_ok &= check(False, "CLOB auth/collateral", str(e)[:160])
164
+
165
+ # 5. optional market checks
166
+ if market_arg:
167
+ pm = parse_market(get_market(market_arg))
168
+ if pm:
169
+ b = get_book(pm["token_a"])
170
+ meta = book_meta(b)
171
+ bid, _, ask, _ = best_bid_ask(b)
172
+ print(f"{GREEN} market {market_arg}: bid={bid} ask={ask} "
173
+ f"min_order_size={meta['min_order_size']} tick={meta['tick_size']}")
174
+ if ask is not None:
175
+ print(f" taker fee at ask: {fee(ask, 1.0):.4f}$/share (crypto table; "
176
+ f"authoritative per-market rate via executor.fee_rate)")
177
+ if meta["min_order_size"] and ask:
178
+ print(f" smallest possible order here: about "
179
+ f"{meta['min_order_size'] * ask:.2f}$")
180
+ else:
181
+ all_ok &= check(False, f"market {market_arg} resolvable",
182
+ "expired or wrong slug; recurring families need the window "
183
+ "start suffix, e.g. btc-updown-15m-<unix_ts>")
184
+
185
+ print("\nverdict:", "everything green, orders should work" if all_ok else
186
+ "fix the [!!] lines above, in order")
187
+ return 0 if all_ok else 1
188
+
189
+
190
+ if __name__ == "__main__":
191
+ raise SystemExit(main())
@@ -84,3 +84,14 @@ def test_book_inferred_winner():
84
84
  assert book_inferred_winner(0.02, 0.91) == "b"
85
85
  assert book_inferred_winner(0.60, 0.35) is None
86
86
  assert book_inferred_winner(None, None) is None
87
+
88
+
89
+ def test_doctor_pure_logic():
90
+ from pmq.doctor import advise_sig_type, looks_like_minimal_proxy
91
+ assert looks_like_minimal_proxy("0x363d3d373d3d363d6020366004")
92
+ assert not looks_like_minimal_proxy("0x")
93
+ assert not looks_like_minimal_proxy(None)
94
+ assert advise_sig_type(False, False, True)[0] == 0
95
+ assert advise_sig_type(True, True, False)[0] == 3
96
+ assert advise_sig_type(True, False, False)[0] is None
97
+ assert advise_sig_type(False, False, False)[0] is None
@@ -0,0 +1,26 @@
1
+ import pmq.doctor as doctor
2
+
3
+
4
+ def test_advise_sig_type_matrix():
5
+ assert doctor.advise_sig_type(False, False, True)[0] == 0
6
+ assert doctor.advise_sig_type(True, True, False)[0] == 3
7
+ assert doctor.advise_sig_type(True, False, False)[0] is None
8
+ assert doctor.advise_sig_type(False, False, False)[0] is None
9
+
10
+
11
+ def test_minimal_proxy_detector():
12
+ assert doctor.looks_like_minimal_proxy("0x363d3d373d3d363d6020" + "ab" * 60)
13
+ assert not doctor.looks_like_minimal_proxy("0x")
14
+ assert not doctor.looks_like_minimal_proxy(None)
15
+ assert not doctor.looks_like_minimal_proxy("0x" + "ab" * 300)
16
+
17
+
18
+ def test_main_without_key_exits_1(monkeypatch, capsys):
19
+ monkeypatch.delenv("POLY_PRIVATE_KEY", raising=False)
20
+ assert doctor.main([]) == 1
21
+ assert "POLY_PRIVATE_KEY" in capsys.readouterr().out
22
+
23
+
24
+ def test_main_market_flag_without_value_exits_2(capsys):
25
+ assert doctor.main(["--market"]) == 2
26
+ assert "usage" in capsys.readouterr().out
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