pmquant 0.4.3__tar.gz → 0.4.5__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 (57) hide show
  1. pmquant-0.4.5/.githooks/pre-push +10 -0
  2. pmquant-0.4.5/.github/workflows/codeql.yml +21 -0
  3. pmquant-0.4.5/.github/workflows/mcp-publish.yml +35 -0
  4. {pmquant-0.4.3 → pmquant-0.4.5}/.github/workflows/publish.yml +3 -0
  5. pmquant-0.4.3/.hypothesis/constants/1720e64af9235558 → pmquant-0.4.5/.hypothesis/constants/94e671d33554da02 +1 -1
  6. pmquant-0.4.5/.hypothesis/constants/e9435504a7ce4829 +4 -0
  7. {pmquant-0.4.3 → pmquant-0.4.5}/.hypothesis/unicode_data/15.0.0/charmap.json.gz +0 -0
  8. pmquant-0.4.5/.hypothesis/unicode_data/15.0.0/codec-utf-8.json.gz +0 -0
  9. {pmquant-0.4.3 → pmquant-0.4.5}/CHANGELOG.md +30 -0
  10. {pmquant-0.4.3 → pmquant-0.4.5}/CLAUDE.md +30 -10
  11. pmquant-0.4.5/CONTRIBUTING.md +80 -0
  12. {pmquant-0.4.3 → pmquant-0.4.5}/PKG-INFO +16 -5
  13. {pmquant-0.4.3 → pmquant-0.4.5}/README.md +15 -4
  14. {pmquant-0.4.3 → pmquant-0.4.5}/SECURITY.md +3 -2
  15. {pmquant-0.4.3 → pmquant-0.4.5}/docs/rounding-study.md +18 -0
  16. {pmquant-0.4.3 → pmquant-0.4.5}/pyproject.toml +1 -1
  17. {pmquant-0.4.3 → pmquant-0.4.5}/server.json +3 -3
  18. {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/__init__.py +1 -1
  19. {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/executor.py +32 -0
  20. {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_canary_live.py +2 -1
  21. {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_executor.py +14 -0
  22. pmquant-0.4.3/.hypothesis/constants/07a2a0eac57d1dd0 +0 -4
  23. pmquant-0.4.3/.hypothesis/unicode_data/15.0.0/codec-utf-8.json.gz +0 -0
  24. pmquant-0.4.3/CONTRIBUTING.md +0 -52
  25. {pmquant-0.4.3 → pmquant-0.4.5}/.github/dependabot.yml +0 -0
  26. {pmquant-0.4.3 → pmquant-0.4.5}/.github/workflows/canary.yml +0 -0
  27. {pmquant-0.4.3 → pmquant-0.4.5}/.github/workflows/scorecard.yml +0 -0
  28. {pmquant-0.4.3 → pmquant-0.4.5}/.github/workflows/test.yml +0 -0
  29. {pmquant-0.4.3 → pmquant-0.4.5}/.gitignore +0 -0
  30. {pmquant-0.4.3 → pmquant-0.4.5}/.hypothesis/.gitignore +0 -0
  31. {pmquant-0.4.3 → pmquant-0.4.5}/.hypothesis/constants/6c9ffb0a1efc27b6 +0 -0
  32. {pmquant-0.4.3 → pmquant-0.4.5}/.hypothesis/constants/855d9c2e5b4693f1 +0 -0
  33. {pmquant-0.4.3 → pmquant-0.4.5}/.hypothesis/constants/ef909bf87e6ac33f +0 -0
  34. {pmquant-0.4.3 → pmquant-0.4.5}/AGENTS.md +0 -0
  35. {pmquant-0.4.3 → pmquant-0.4.5}/LICENSE +0 -0
  36. {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/README.md +0 -0
  37. {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/bot.py +0 -0
  38. {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/dash/bot_dash.py +0 -0
  39. {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/dash/dash.html +0 -0
  40. {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/pmq-bot.service +0 -0
  41. {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/strategy.py +0 -0
  42. {pmquant-0.4.3 → pmquant-0.4.5}/docs/assets/pmq-doctor.svg +0 -0
  43. {pmquant-0.4.3 → pmquant-0.4.5}/docs/recipes.md +0 -0
  44. {pmquant-0.4.3 → pmquant-0.4.5}/docs/war-story.md +0 -0
  45. {pmquant-0.4.3 → pmquant-0.4.5}/examples/fak_buy_guarded.py +0 -0
  46. {pmquant-0.4.3 → pmquant-0.4.5}/examples/read_market.py +0 -0
  47. {pmquant-0.4.3 → pmquant-0.4.5}/llms.txt +0 -0
  48. {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/data.py +0 -0
  49. {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/doctor.py +0 -0
  50. {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/exceptions.py +0 -0
  51. {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/mcp.py +0 -0
  52. {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/py.typed +0 -0
  53. {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_data.py +0 -0
  54. {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_doctor.py +0 -0
  55. {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_fill_fuzz.py +0 -0
  56. {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_mcp.py +0 -0
  57. {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_template_engine.py +0 -0
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+ # pmq pre-push guard: refuse to push red. CI is the backstop; this catches
3
+ # it before the remote does. Bypass knowingly with --no-verify.
4
+ set -e
5
+ cd "$(git rev-parse --show-toplevel)"
6
+ PY=./.venv/bin/python
7
+ [ -x "$PY" ] || PY=python3
8
+ $PY -m ruff check . || { echo "pre-push: ruff rouge"; exit 1; }
9
+ $PY -m mypy || { echo "pre-push: mypy rouge"; exit 1; }
10
+ $PY -m pytest -q || { echo "pre-push: tests rouges"; exit 1; }
@@ -0,0 +1,21 @@
1
+ name: codeql
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ schedule:
7
+ - cron: "41 7 * * 1"
8
+
9
+ permissions: read-all
10
+
11
+ jobs:
12
+ analyze:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ security-events: write
16
+ steps:
17
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
18
+ - uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4
19
+ with:
20
+ languages: python
21
+ - uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4
@@ -0,0 +1,35 @@
1
+ name: mcp-publish
2
+ on:
3
+ release:
4
+ types: [published]
5
+ workflow_dispatch:
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write # OIDC login to the MCP registry
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
16
+ - name: Fetch mcp-publisher
17
+ env:
18
+ GH_TOKEN: ${{ github.token }}
19
+ run: |
20
+ gh release download --repo modelcontextprotocol/registry \
21
+ --pattern "mcp-publisher_*linux_amd64.tar.gz" --output mp.tgz
22
+ tar -xzf mp.tgz
23
+ ./mcp-publisher --version
24
+ - name: Wait for the version to exist on PyPI
25
+ run: |
26
+ V=$(python3 -c "import json; print(json.load(open('server.json'))['version'])")
27
+ for i in $(seq 1 20); do
28
+ curl -s "https://pypi.org/pypi/pmquant/$V/json" | grep -q '"version"' && exit 0
29
+ echo "PyPI does not serve $V yet ($i/20)"; sleep 30
30
+ done
31
+ echo "giving up: registry would reject an unpublished version"; exit 1
32
+ - name: Publish server.json to the MCP registry
33
+ run: |
34
+ ./mcp-publisher login github-oidc
35
+ ./mcp-publisher publish
@@ -5,6 +5,9 @@ on:
5
5
  types: [published]
6
6
  workflow_dispatch:
7
7
 
8
+ permissions:
9
+ contents: read
10
+
8
11
  jobs:
9
12
  publish:
10
13
  runs-on: ubuntu-latest
@@ -1,4 +1,4 @@
1
1
  # file: /home/runner/work/pmq/pmq/src/pmq/__init__.py
2
2
  # hypothesis_version: 6.156.1
3
3
 
4
- ['0.4.3', 'DEFAULT_BUILDER_CODE', 'FEE_RATES', 'Fill', 'OrderUncertain', 'PmqError', 'PolymarketExecutor', '__version__', 'band_ask_depth_usd', 'best_bid_ask', 'book_inferred_winner', 'book_meta', 'event_markets', 'fee', 'get_book', 'get_market', 'get_tape', 'http_get_json', 'parse_market', 'positions', 'resolved_winner']
4
+ ['0.4.5', 'DEFAULT_BUILDER_CODE', 'FEE_RATES', 'Fill', 'OrderUncertain', 'PmqError', 'PolymarketExecutor', '__version__', 'band_ask_depth_usd', 'best_bid_ask', 'book_inferred_winner', 'book_meta', 'event_markets', 'fee', 'get_book', 'get_market', 'get_tape', 'http_get_json', 'parse_market', 'positions', 'resolved_winner']
@@ -0,0 +1,4 @@
1
+ # file: /home/runner/work/pmq/pmq/src/pmq/executor.py
2
+ # hypothesis_version: 6.156.1
3
+
4
+ [0.0, 0.933, 0.983, 0.985, 4.97, 5.35, 9.98, 10.13, 1000000.0, 100, 137, 300, 400, 500, 10000, '0', '0.01', '0x[0-9a-fA-F]{64}', '; ', 'AssetType', 'BUY', 'FAILED', 'FAK', 'MAKER', 'MarketOrderArgs', 'MarketOrderArgsV2', 'OpenOrderParams', 'OrderArgs', 'OrderArgsV2', 'OrderType', 'POLY_BUILDER_CODE', 'POLY_FUNDER', 'POLY_PRIVATE_KEY', 'POLY_SIG_TYPE', 'SELL', 'TradeParams', '_pmq_taker4', 'amount', 'balance', 'builder_code', 'cancel_market_orders', 'crypto', 'error_msg', 'fd', 'get_open_orders', 'get_trades', 'makingAmount', 'off', 'on', 'orderID', 'order_args', 'order_type', 'params', 'payload', 'pmq', 'price', 'r', 'side', 'size', 'status', 'status_code', 'success', 'takingAmount', 'token_id', 'trader_side']
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.5 (2026-07-04)
4
+
5
+ * Startup guard against the fine-tick market-order rejection class: the
6
+ introspection now exercises the installed builder's amount arithmetic
7
+ across every rounding config and refuses to construct an executor that
8
+ would sign a market pair above the exchange caps (2 decimal maker,
9
+ 4 decimal taker). A client build that slips past the 0.4.3 clamp fails
10
+ at deploy time, before any order.
11
+ * Honesty pass after the 2026-07-04 production halt: the README rounding
12
+ bullet now states the fine-tick failure mode and its dates, and
13
+ docs/rounding-study.md gains an addendum scoping the July 3 conclusions
14
+ to ticks >= 0.01.
15
+
16
+ ## 0.4.4 (2026-07-04)
17
+
18
+ * Harden: json.loads accepts NaN and Infinity, so a drifted or hostile
19
+ exchange response could book non-finite or negative matched amounts.
20
+ `_parse_fill` now zeroes anything non-finite or negative (fail closed),
21
+ and a hypothesis fuzz suite (four property groups, hundreds of generated
22
+ adversarial responses per run) pins the whole fill contract: market and
23
+ limit paths book only confirmed finite amounts, the 4xx/uncertain
24
+ exception partition is total, every transport exception surfaces as
25
+ OrderUncertain.
26
+ * Security surface: CodeQL workflow (its first scan caught and we fixed a
27
+ host-boundary bypass in the egress allowlist), Scorecard alert triage
28
+ with written dismissal reasons, top-level permissions on the publish
29
+ workflow, direct private-advisory link in SECURITY.md, Dependabot
30
+ vulnerability alerts enabled. Listed in the official MCP registry as
31
+ io.github.crp4222/pmq (publish rides releases via OIDC).
32
+
3
33
  ## 0.4.3 (2026-07-04)
4
34
 
5
35
  * Fix: py-clob-client-v2 1.0.2 reuses its limit-order rounding table for
@@ -9,16 +9,26 @@ agents EDITING it. Read both before changing code.)
9
9
  confirmed (`orderID` + `success is not False` + matched amounts); 4xx is a
10
10
  clean rejection; timeout/5xx raises `OrderUncertain`; unparseable = zero.
11
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.
12
+ whatever it fixes elsewhere. Matched amounts must be finite and
13
+ non-negative (json.loads accepts NaN/Infinity; hostile values book zero).
14
+ `reconcile()` must keep meaning cancel + `get_trades` truth. The
15
+ hypothesis fuzz suite (tests/test_fill_fuzz.py) pins all of this with
16
+ generated adversarial responses: extend it with every parser change,
17
+ never delete it.
14
18
  2. **Startup introspection** (`_EXPECTED_METHODS`/`_EXPECTED_MARKET_ARGS`):
15
19
  the executor REFUSES to run on a drifted py-clob-client-v2. When bumping
16
20
  the client dependency, re-verify signatures by introspection and update
17
21
  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
+ 3. **Builder code policy**: default = maintainer's code, defined in exactly
23
+ ONE place (`DEFAULT_BUILDER_CODE` in executor.py) and applied
24
+ automatically by every order path. DISCLOSED in README and code comment,
25
+ opt-out one line (`builder_code=None` / env). Never hide it, never remove
26
+ the disclosure, never make opt-out harder. AND the mirror rule: keep the
27
+ disclosure at the DOCUMENTATION level only; do not surface attribution in
28
+ runtime channels (server startup logs, MCP tools or instructions, order
29
+ responses). It is public on-chain in every signed order; in-band
30
+ reminders just prompt sessions to toggle a setting that costs users
31
+ nothing. This is the trust model (JKorf pattern).
22
32
  4. **No strategy content, ever**: the maintainer's private bot strategy
23
33
  (bands, timing, hours, families, sizing) must never appear in code, docs,
24
34
  tests, commits or issues. The bot-template ships deliberately naive
@@ -41,10 +51,20 @@ agents EDITING it. Read both before changing code.)
41
51
  * Exchange rules (min size, tick, fee rate) are READ from the venue
42
52
  (`book_meta`, `fee_rate`), not hardcoded. `FEE_RATES` is a documented
43
53
  snapshot of the official schedule used for estimates.
44
- * Releases: bump version in `pyproject.toml` AND `src/pmq/__init__.py`,
45
- update CHANGELOG.md, push, then `gh release create vX.Y.Z`: PyPI publish
46
- is automatic via trusted publishing (no tokens anywhere). PyPI name is
47
- `pmquant`, import name `pmq`: keep the README line explaining it.
54
+ * Releases: bump version in `pyproject.toml`, `src/pmq/__init__.py` AND
55
+ `server.json` (both version fields), update CHANGELOG.md, push, then
56
+ `gh release create vX.Y.Z`: PyPI publish (trusted publishing, signed
57
+ attestations) and the MCP registry republish (mcp-publish.yml,
58
+ github-oidc) both fire on the release event. Registry gotchas: the
59
+ server.json description caps at 100 characters, and the version must
60
+ exist on PyPI. PyPI name is `pmquant`, import name `pmq`: keep the
61
+ README line explaining it.
62
+ * CLAUDE.md and CONTRIBUTING.md are THE SAME FILE by contract: after
63
+ editing one, copy it over the other in the same commit (`cp CLAUDE.md
64
+ CONTRIBUTING.md`). Drift between them means an agent read stale rules.
65
+ * Local guard: `git config core.hooksPath .githooks` once per clone
66
+ enables the pre-push hook (ruff + mypy + pytest). CI is the backstop,
67
+ but the hook catches a broken push before it lands.
48
68
  * GitHub Actions stay pinned by commit SHA (dependabot bumps them); new
49
69
  workflows get an explicit least-privilege permissions block. The egress
50
70
  test and pip-audit ride the weekly canary: never move them to default CI
@@ -0,0 +1,80 @@
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. Matched amounts must be finite and
13
+ non-negative (json.loads accepts NaN/Infinity; hostile values book zero).
14
+ `reconcile()` must keep meaning cancel + `get_trades` truth. The
15
+ hypothesis fuzz suite (tests/test_fill_fuzz.py) pins all of this with
16
+ generated adversarial responses: extend it with every parser change,
17
+ never delete it.
18
+ 2. **Startup introspection** (`_EXPECTED_METHODS`/`_EXPECTED_MARKET_ARGS`):
19
+ the executor REFUSES to run on a drifted py-clob-client-v2. When bumping
20
+ the client dependency, re-verify signatures by introspection and update
21
+ the tables in the same commit.
22
+ 3. **Builder code policy**: default = maintainer's code, defined in exactly
23
+ ONE place (`DEFAULT_BUILDER_CODE` in executor.py) and applied
24
+ automatically by every order path. DISCLOSED in README and code comment,
25
+ opt-out one line (`builder_code=None` / env). Never hide it, never remove
26
+ the disclosure, never make opt-out harder. AND the mirror rule: keep the
27
+ disclosure at the DOCUMENTATION level only; do not surface attribution in
28
+ runtime channels (server startup logs, MCP tools or instructions, order
29
+ responses). It is public on-chain in every signed order; in-band
30
+ reminders just prompt sessions to toggle a setting that costs users
31
+ nothing. This is the trust model (JKorf pattern).
32
+ 4. **No strategy content, ever**: the maintainer's private bot strategy
33
+ (bands, timing, hours, families, sizing) must never appear in code, docs,
34
+ tests, commits or issues. The bot-template ships deliberately naive
35
+ demos only.
36
+ 5. **Claims must be falsifiable**: no superlatives in README/docs; dated
37
+ claims with evidence (comparison table, on-chain receipts, measured
38
+ studies). If you cannot prove it, do not write it.
39
+ 6. **MCP safety gates**: trading tools are REGISTERED only when the operator
40
+ sets `PMQ_MCP_LIVE=1`; per-order `PMQ_MCP_MAX_USD` cap enforced before
41
+ any client call. Read tools must keep working with zero credentials.
42
+
43
+ ## Working rules
44
+
45
+ * Tests green (`pytest -q`) and `ruff check .` clean before any push;
46
+ `pyscn check src/pmq bot-template` (complexity <= 10, no dead code)
47
+ must stay green too; clone warnings are informational (the template
48
+ dash deliberately duplicates helpers to stay stdlib-standalone). Add
49
+ tests with every behavior change. Network-touching tests go to
50
+ `tests/test_canary_live.py` behind `PMQ_CANARY=1`, never in default CI.
51
+ * Exchange rules (min size, tick, fee rate) are READ from the venue
52
+ (`book_meta`, `fee_rate`), not hardcoded. `FEE_RATES` is a documented
53
+ snapshot of the official schedule used for estimates.
54
+ * Releases: bump version in `pyproject.toml`, `src/pmq/__init__.py` AND
55
+ `server.json` (both version fields), update CHANGELOG.md, push, then
56
+ `gh release create vX.Y.Z`: PyPI publish (trusted publishing, signed
57
+ attestations) and the MCP registry republish (mcp-publish.yml,
58
+ github-oidc) both fire on the release event. Registry gotchas: the
59
+ server.json description caps at 100 characters, and the version must
60
+ exist on PyPI. PyPI name is `pmquant`, import name `pmq`: keep the
61
+ README line explaining it.
62
+ * CLAUDE.md and CONTRIBUTING.md are THE SAME FILE by contract: after
63
+ editing one, copy it over the other in the same commit (`cp CLAUDE.md
64
+ CONTRIBUTING.md`). Drift between them means an agent read stale rules.
65
+ * Local guard: `git config core.hooksPath .githooks` once per clone
66
+ enables the pre-push hook (ruff + mypy + pytest). CI is the backstop,
67
+ but the hook catches a broken push before it lands.
68
+ * GitHub Actions stay pinned by commit SHA (dependabot bumps them); new
69
+ workflows get an explicit least-privilege permissions block. The egress
70
+ test and pip-audit ride the weekly canary: never move them to default CI
71
+ (they need network) and never widen the egress allowlist beyond
72
+ polymarket.com without updating SECURITY.md and the README section.
73
+ * The weekly canary workflow is the drift alarm: if it opens an issue, the
74
+ fix starts by re-running the introspection against the new surface, not
75
+ by loosening the checks.
76
+ * Keep the library small and auditable (five modules): resist adding
77
+ dependencies; stdlib first. Anything bot-shaped belongs in bot-template/,
78
+ not in the package.
79
+ * Style: no em-dashes and no " - " connectors anywhere (strong user rule);
80
+ keep comments sparse and constraint-focused.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pmquant
3
- Version: 0.4.3
3
+ Version: 0.4.5
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
@@ -71,8 +71,14 @@ examples leave several traps undocumented. Every line of pmq was paid for with
71
71
  a real error in live trading:
72
72
 
73
73
  * `invalid amounts, the market buy orders maker amount supports a max accuracy
74
- of 2 decimals`: the CLOB treats FAK/FOK buys as **market orders**. pmq routes
75
- them through the market-order builder with the correct rounding.
74
+ of 2 decimals, taker amount a max of 4 decimals`: the CLOB treats FAK/FOK
75
+ buys as **market orders** and caps their signed amounts at 2 decimals
76
+ (maker) / 4 decimals (taker) whatever the tick size. The official client's
77
+ rounding table allows 5-6 taker decimals on ticks finer than 0.01, so every
78
+ market order there is rejected; this cost us a production halt on
79
+ 2026-07-04. pmq clamps the signed pair to the exchange caps (0.4.3) and
80
+ refuses at startup any client build that would still sign a rejectable
81
+ pair (0.4.5).
76
82
  * `no orders found to match with FAK order` (HTTP 400, yet with an `orderID`):
77
83
  a clean no-fill, not an error. pmq returns an empty `Fill` instead of crashing
78
84
  or, worse, retrying blindly.
@@ -130,7 +136,10 @@ not your hopes.
130
136
 
131
137
  At startup pmq **introspects the installed py-clob-client-v2** against the API
132
138
  surface it was verified on, and refuses to trade on drift instead of sending
133
- orders through changed semantics.
139
+ orders through changed semantics. The whole table is pinned by an executable
140
+ test per row plus a hypothesis fuzz suite (hundreds of generated adversarial
141
+ responses per run, including NaN/Infinity and negative amounts, which book
142
+ zero).
134
143
 
135
144
  ## Quickstart
136
145
 
@@ -217,7 +226,9 @@ JKorf/Polymarket.Net; the official client defaults to zero attribution.)
217
226
 
218
227
  ## MCP server (agents)
219
228
 
220
- `pip install "pmquant[mcp]"` then run `pmq-mcp` (stdio). Read tools (market,
229
+ `pip install "pmquant[mcp]"` then run `pmq-mcp` (stdio). Listed in the
230
+ [official MCP registry](https://registry.modelcontextprotocol.io) as
231
+ `io.github.crp4222/pmq`. Read tools (market,
221
232
  book, taker_fee, account_collateral, account_trades) always exist. Trading
222
233
  tools (`fak_buy`, `fak_sell`, `cancel_and_reconcile`) are **only registered
223
234
  when the operator sets `PMQ_MCP_LIVE=1`** in the server environment: an
@@ -36,8 +36,14 @@ examples leave several traps undocumented. Every line of pmq was paid for with
36
36
  a real error in live trading:
37
37
 
38
38
  * `invalid amounts, the market buy orders maker amount supports a max accuracy
39
- of 2 decimals`: the CLOB treats FAK/FOK buys as **market orders**. pmq routes
40
- them through the market-order builder with the correct rounding.
39
+ of 2 decimals, taker amount a max of 4 decimals`: the CLOB treats FAK/FOK
40
+ buys as **market orders** and caps their signed amounts at 2 decimals
41
+ (maker) / 4 decimals (taker) whatever the tick size. The official client's
42
+ rounding table allows 5-6 taker decimals on ticks finer than 0.01, so every
43
+ market order there is rejected; this cost us a production halt on
44
+ 2026-07-04. pmq clamps the signed pair to the exchange caps (0.4.3) and
45
+ refuses at startup any client build that would still sign a rejectable
46
+ pair (0.4.5).
41
47
  * `no orders found to match with FAK order` (HTTP 400, yet with an `orderID`):
42
48
  a clean no-fill, not an error. pmq returns an empty `Fill` instead of crashing
43
49
  or, worse, retrying blindly.
@@ -95,7 +101,10 @@ not your hopes.
95
101
 
96
102
  At startup pmq **introspects the installed py-clob-client-v2** against the API
97
103
  surface it was verified on, and refuses to trade on drift instead of sending
98
- orders through changed semantics.
104
+ orders through changed semantics. The whole table is pinned by an executable
105
+ test per row plus a hypothesis fuzz suite (hundreds of generated adversarial
106
+ responses per run, including NaN/Infinity and negative amounts, which book
107
+ zero).
99
108
 
100
109
  ## Quickstart
101
110
 
@@ -182,7 +191,9 @@ JKorf/Polymarket.Net; the official client defaults to zero attribution.)
182
191
 
183
192
  ## MCP server (agents)
184
193
 
185
- `pip install "pmquant[mcp]"` then run `pmq-mcp` (stdio). Read tools (market,
194
+ `pip install "pmquant[mcp]"` then run `pmq-mcp` (stdio). Listed in the
195
+ [official MCP registry](https://registry.modelcontextprotocol.io) as
196
+ `io.github.crp4222/pmq`. Read tools (market,
186
197
  book, taker_fee, account_collateral, account_trades) always exist. Trading
187
198
  tools (`fak_buy`, `fak_sell`, `cancel_and_reconcile`) are **only registered
188
199
  when the operator sets `PMQ_MCP_LIVE=1`** in the server environment: an
@@ -35,6 +35,7 @@ the important questions:
35
35
 
36
36
  ## Reporting a vulnerability
37
37
 
38
- Open a GitHub security advisory on this repository (Security tab, "Report a
39
- vulnerability") or an issue with the `security` label if it is not sensitive.
38
+ Open a [private security advisory](https://github.com/crp4222/pmq/security/advisories/new)
39
+ (Security tab, "Report a vulnerability") or, if it is not sensitive, an
40
+ [issue](https://github.com/crp4222/pmq/issues) with the `security` label.
40
41
  You will get an answer within a few days.
@@ -50,3 +50,21 @@ amounts against requested size, cross-checked with `get_trades`.
50
50
  4. The per-market minimum size is enforced server-side with a clean 400, and
51
51
  is readable in advance from the book response (`min_order_size`, exposed
52
52
  by `pmq.book_meta`).
53
+
54
+ ## Addendum (2026-07-04): fine-tick market orders
55
+
56
+ The study above ran on a 0.01-tick market and point 1 turns out to hold
57
+ only there. On 2026-07-04 a 0.001-tick market rejected every market buy
58
+ with `invalid amounts ... taker amount a max of 4 decimals`: the client's
59
+ ROUNDING_CONFIG allows amount decimals = price decimals + 2 (5 for tick
60
+ 0.001, 6 for 0.0025 and 0.0001). That is right for LIMIT orders, whose
61
+ amounts are exact price×size products, but MARKET-order takers are capped
62
+ by the server at a flat 4 decimals whatever the tick, so on fine ticks the
63
+ maker/price division leaves a 5th decimal and the order can never be
64
+ accepted. Normalization still happens client-side; it just normalizes to a
65
+ precision the server refuses.
66
+
67
+ pmq 0.4.3 clamps the market path to 4 decimals (round-down: the budget
68
+ contract is intact, the dust given up is under 0.0001 share). pmq 0.4.5
69
+ also refuses at startup any client build that would still sign such a
70
+ pair, so this class of failure stops at deploy time, not at trade time.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pmquant"
7
- version = "0.4.3"
7
+ version = "0.4.5"
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" }
@@ -2,18 +2,18 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.crp4222/pmq",
4
4
  "title": "pmq (Polymarket CLOB V2)",
5
- "description": "Fail-closed Polymarket CLOB V2 market data and execution, production-proven daily with the maintainer's own funds. Exchange-confirmed fills only; keys never leave the process (weekly public CI egress proof: only polymarket.com is ever contacted). Read tools need zero credentials; trading tools exist only when the operator sets PMQ_MCP_LIVE=1, capped per order by PMQ_MCP_MAX_USD.",
5
+ "description": "Production-proven Polymarket CLOB V2 trading and data. Fail-closed fills; keys stay local.",
6
6
  "repository": {
7
7
  "url": "https://github.com/crp4222/pmq",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.4.2",
10
+ "version": "0.4.4",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "registryBaseUrl": "https://pypi.org",
15
15
  "identifier": "pmquant",
16
- "version": "0.4.2",
16
+ "version": "0.4.4",
17
17
  "runtimeHint": "uvx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -25,7 +25,7 @@ from .data import (
25
25
  )
26
26
  from .exceptions import IntrospectionMismatch, OrderUncertain, PmqError
27
27
 
28
- __version__ = "0.4.3"
28
+ __version__ = "0.4.5"
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",
@@ -218,8 +218,40 @@ class PolymarketExecutor:
218
218
  drifts += [f"{label} lost field {p}" for p in expected if p not in have]
219
219
  if not hasattr(self._t["OrderType"], "FAK"):
220
220
  drifts.append("OrderType.FAK missing")
221
+ drifts += self._amount_precision_drifts()
221
222
  return drifts
222
223
 
224
+ def _amount_precision_drifts(self) -> list[str]:
225
+ """Behavioral guard: the exchange caps signed MARKET-order amounts at
226
+ 2 decimals (maker) and 4 (taker) whatever the tick size. Exercise the
227
+ installed builder's arithmetic on awkward pairs across every rounding
228
+ config; a client build that would sign a rejectable pair is refused
229
+ here, at startup, instead of failing on the first fine-tick order."""
230
+ try:
231
+ from py_clob_client_v2.order_builder.builder import (
232
+ ROUNDING_CONFIG,
233
+ OrderBuilder,
234
+ )
235
+ from py_clob_client_v2.order_builder.constants import BUY, SELL
236
+ except ImportError:
237
+ return []
238
+ out: set[str] = set()
239
+ probe = object.__new__(OrderBuilder)
240
+ cases = ((BUY, 9.98, 0.985), (BUY, 4.97, 0.983),
241
+ (SELL, 10.13, 0.985), (SELL, 5.35, 0.933))
242
+ for tick, rc in ROUNDING_CONFIG.items():
243
+ for side, amount, price in cases:
244
+ try:
245
+ _, mk, tk = OrderBuilder.get_market_order_amounts(
246
+ probe, side, amount, price, rc)
247
+ except Exception as e:
248
+ out.add(f"market amounts builder failed at tick {tick}: {e}")
249
+ continue
250
+ if int(mk) % 10_000 or int(tk) % 100:
251
+ out.add(f"market order would sign >2dp maker or >4dp taker "
252
+ f"at tick {tick}")
253
+ return sorted(out)
254
+
223
255
  def _verify_client_surface(self) -> None:
224
256
  drifts = self._surface_drifts()
225
257
  if drifts:
@@ -101,5 +101,6 @@ def test_egress_only_polymarket_hosts(monkeypatch):
101
101
  except pmq.OrderUncertain:
102
102
  pass # 5xx path; egress is the point
103
103
  print("hosts contacted:", sorted(hosts))
104
- foreign = {h for h in hosts if not h.endswith("polymarket.com")}
104
+ foreign = {h for h in hosts
105
+ if h != "polymarket.com" and not h.endswith(".polymarket.com")}
105
106
  assert not foreign, f"unexpected egress: {sorted(foreign)}"
@@ -362,3 +362,17 @@ def test_market_taker_amounts_clamped_to_4dp_on_fine_ticks():
362
362
  assert int(tk) % 10**2 == 0, f"taker >4dp at tick {tick}"
363
363
  _, mk_s, tk_s = fn(b, SELL, 10.13, 0.985, ROUNDING_CONFIG[tick])
364
364
  assert int(mk_s) % 10**4 == 0 and int(tk_s) % 10**2 == 0
365
+
366
+
367
+ def test_startup_refuses_client_signing_dirty_market_amounts(monkeypatch):
368
+ """If a client build slips past the 4dp clamp (new code path, future
369
+ regression), the startup introspection must refuse to trade at all."""
370
+ from py_clob_client_v2.order_builder import builder as b
371
+
372
+ def dirty(self, side, amount, price, round_config):
373
+ return side, 9980000, 10131970 # taker 10.13197: 5 decimals
374
+
375
+ dirty._pmq_taker4 = True # defeat the pmq wrapper
376
+ monkeypatch.setattr(b.OrderBuilder, "get_market_order_amounts", dirty)
377
+ with pytest.raises(IntrospectionMismatch):
378
+ make(FakeClient())
@@ -1,4 +0,0 @@
1
- # file: /home/runner/work/pmq/pmq/src/pmq/executor.py
2
- # hypothesis_version: 6.156.1
3
-
4
- [0.0, 1000000.0, 137, 300, 400, 500, '0', '0.01', '0x[0-9a-fA-F]{64}', '; ', 'AssetType', 'BUY', 'FAILED', 'FAK', 'MAKER', 'MarketOrderArgs', 'MarketOrderArgsV2', 'OpenOrderParams', 'OrderArgs', 'OrderArgsV2', 'OrderType', 'POLY_BUILDER_CODE', 'POLY_FUNDER', 'POLY_PRIVATE_KEY', 'POLY_SIG_TYPE', 'SELL', 'TradeParams', '_pmq_taker4', 'amount', 'balance', 'builder_code', 'cancel_market_orders', 'crypto', 'error_msg', 'fd', 'get_open_orders', 'get_trades', 'makingAmount', 'off', 'on', 'orderID', 'order_args', 'order_type', 'params', 'payload', 'pmq', 'price', 'r', 'side', 'size', 'status', 'status_code', 'success', 'takingAmount', 'token_id', 'trader_side']
@@ -1,52 +0,0 @@
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.
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
File without changes
File without changes