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.
- pmquant-0.4.5/.githooks/pre-push +10 -0
- pmquant-0.4.5/.github/workflows/codeql.yml +21 -0
- pmquant-0.4.5/.github/workflows/mcp-publish.yml +35 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/.github/workflows/publish.yml +3 -0
- pmquant-0.4.3/.hypothesis/constants/1720e64af9235558 → pmquant-0.4.5/.hypothesis/constants/94e671d33554da02 +1 -1
- pmquant-0.4.5/.hypothesis/constants/e9435504a7ce4829 +4 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/.hypothesis/unicode_data/15.0.0/charmap.json.gz +0 -0
- pmquant-0.4.5/.hypothesis/unicode_data/15.0.0/codec-utf-8.json.gz +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/CHANGELOG.md +30 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/CLAUDE.md +30 -10
- pmquant-0.4.5/CONTRIBUTING.md +80 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/PKG-INFO +16 -5
- {pmquant-0.4.3 → pmquant-0.4.5}/README.md +15 -4
- {pmquant-0.4.3 → pmquant-0.4.5}/SECURITY.md +3 -2
- {pmquant-0.4.3 → pmquant-0.4.5}/docs/rounding-study.md +18 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/pyproject.toml +1 -1
- {pmquant-0.4.3 → pmquant-0.4.5}/server.json +3 -3
- {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/__init__.py +1 -1
- {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/executor.py +32 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_canary_live.py +2 -1
- {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_executor.py +14 -0
- pmquant-0.4.3/.hypothesis/constants/07a2a0eac57d1dd0 +0 -4
- pmquant-0.4.3/.hypothesis/unicode_data/15.0.0/codec-utf-8.json.gz +0 -0
- pmquant-0.4.3/CONTRIBUTING.md +0 -52
- {pmquant-0.4.3 → pmquant-0.4.5}/.github/dependabot.yml +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/.github/workflows/canary.yml +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/.github/workflows/scorecard.yml +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/.github/workflows/test.yml +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/.gitignore +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/.hypothesis/.gitignore +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/.hypothesis/constants/6c9ffb0a1efc27b6 +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/.hypothesis/constants/855d9c2e5b4693f1 +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/.hypothesis/constants/ef909bf87e6ac33f +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/AGENTS.md +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/LICENSE +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/README.md +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/bot.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/dash/bot_dash.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/dash/dash.html +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/pmq-bot.service +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/bot-template/strategy.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/docs/assets/pmq-doctor.svg +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/docs/recipes.md +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/docs/war-story.md +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/examples/fak_buy_guarded.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/examples/read_market.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/llms.txt +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/data.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/doctor.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/exceptions.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/mcp.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/src/pmq/py.typed +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_data.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_doctor.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_fill_fuzz.py +0 -0
- {pmquant-0.4.3 → pmquant-0.4.5}/tests/test_mcp.py +0 -0
- {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
|
|
@@ -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.
|
|
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']
|
|
Binary file
|
|
Binary file
|
|
@@ -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.
|
|
13
|
-
|
|
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,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
45
|
-
update CHANGELOG.md, push, then
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
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
|
|
75
|
-
|
|
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).
|
|
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
|
|
40
|
-
|
|
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).
|
|
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
|
|
39
|
-
vulnerability") or
|
|
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.
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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']
|
|
Binary file
|
pmquant-0.4.3/CONTRIBUTING.md
DELETED
|
@@ -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
|
|
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
|