pmquant 0.1.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.
- pmquant-0.3.0/.github/workflows/canary.yml +34 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/.github/workflows/publish.yml +11 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/.github/workflows/test.yml +1 -0
- pmquant-0.3.0/CHANGELOG.md +51 -0
- pmquant-0.3.0/CLAUDE.md +52 -0
- pmquant-0.3.0/CONTRIBUTING.md +52 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/PKG-INFO +36 -1
- {pmquant-0.1.0 → pmquant-0.3.0}/README.md +34 -0
- pmquant-0.3.0/SECURITY.md +29 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/bot-template/bot.py +40 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/bot-template/dash/bot_dash.py +2 -1
- {pmquant-0.1.0 → pmquant-0.3.0}/bot-template/dash/dash.html +4 -2
- pmquant-0.3.0/docs/assets/pmq-doctor.svg +17 -0
- pmquant-0.3.0/docs/recipes.md +93 -0
- pmquant-0.3.0/docs/rounding-study.md +52 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/pyproject.toml +11 -2
- {pmquant-0.1.0 → pmquant-0.3.0}/src/pmq/__init__.py +19 -6
- {pmquant-0.1.0 → pmquant-0.3.0}/src/pmq/data.py +23 -0
- pmquant-0.3.0/src/pmq/doctor.py +191 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/src/pmq/executor.py +31 -3
- {pmquant-0.1.0 → pmquant-0.3.0}/src/pmq/mcp.py +13 -0
- pmquant-0.3.0/tests/test_canary_live.py +72 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/tests/test_data.py +19 -2
- pmquant-0.3.0/tests/test_doctor.py +26 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/tests/test_executor.py +18 -0
- pmquant-0.3.0/tests/test_template_engine.py +82 -0
- pmquant-0.1.0/CHANGELOG.md +0 -25
- {pmquant-0.1.0 → pmquant-0.3.0}/.gitignore +0 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/AGENTS.md +0 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/LICENSE +0 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/bot-template/README.md +0 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/bot-template/pmq-bot.service +0 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/bot-template/strategy.py +0 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/docs/war-story.md +0 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/examples/fak_buy_guarded.py +0 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/examples/read_market.py +0 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/llms.txt +0 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/src/pmq/exceptions.py +0 -0
- {pmquant-0.1.0 → pmquant-0.3.0}/tests/test_mcp.py +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: canary
|
|
2
|
+
on:
|
|
3
|
+
schedule:
|
|
4
|
+
- cron: "17 6 * * 1" # weekly, monday 06:17 UTC
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
issues: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
canary:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
- run: pip install -e ".[dev]"
|
|
20
|
+
- name: Live canary against real Polymarket endpoints
|
|
21
|
+
env:
|
|
22
|
+
PMQ_CANARY: "1"
|
|
23
|
+
run: pytest tests/test_canary_live.py -q
|
|
24
|
+
- name: Open an issue if Polymarket drifted
|
|
25
|
+
if: failure()
|
|
26
|
+
env:
|
|
27
|
+
GH_TOKEN: ${{ github.token }}
|
|
28
|
+
run: |
|
|
29
|
+
existing=$(gh issue list --label canary --state open --json number --jq length)
|
|
30
|
+
if [ "$existing" = "0" ]; then
|
|
31
|
+
gh issue create --label canary \
|
|
32
|
+
--title "Canary: Polymarket surface drifted ($(date -u +%F))" \
|
|
33
|
+
--body "The weekly live canary failed: an endpoint shape or the installed py-clob-client-v2 surface no longer matches what pmq was verified against. See the failed run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
34
|
+
fi
|
|
@@ -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
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
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
|
+
|
|
19
|
+
## 0.2.0 (2026-07-03)
|
|
20
|
+
|
|
21
|
+
* New: `positions(user)` and `event_markets(slug)` in the data layer; `event`
|
|
22
|
+
tool in the MCP server; `fee_rate(condition_id)` (authoritative per-market
|
|
23
|
+
taker rate from the exchange) and `cancel_order(order_id)` on the executor.
|
|
24
|
+
* Trust: weekly live canary workflow (real-endpoint checks, auto-opens an
|
|
25
|
+
issue on drift), SECURITY.md, production receipt in the README,
|
|
26
|
+
docs/rounding-study.md (measured V2 rounding behavior).
|
|
27
|
+
* Quality: ruff in CI, tests for the bot-template engine (34 tests total).
|
|
28
|
+
|
|
29
|
+
## 0.1.0 (2026-07-03)
|
|
30
|
+
|
|
31
|
+
First release.
|
|
32
|
+
|
|
33
|
+
* `pmq.data`: real-time books (with per-market exchange rules via
|
|
34
|
+
`book_meta`), gamma slug resolution with expired-market fallback,
|
|
35
|
+
market-agnostic `parse_market` (any binary outcomes, close time), settled
|
|
36
|
+
and book-inferred winners, offline trade tape, official per-category taker
|
|
37
|
+
fee formula.
|
|
38
|
+
* `pmq.executor.PolymarketExecutor`: fail-closed CLOB V2 execution. FAK
|
|
39
|
+
buys/sells through the market-order path, exchange-confirmed fills only,
|
|
40
|
+
`OrderUncertain` + `reconcile()` from get_trades, deposit-wallet
|
|
41
|
+
(POLY_1271) support, collateral fail-fast, builder-code default with
|
|
42
|
+
disclosure and opt-out, startup introspection of the installed
|
|
43
|
+
py-clob-client-v2.
|
|
44
|
+
* `pmq.mcp` (`pmq-mcp`): MCP server. Read tools always available
|
|
45
|
+
(find_markets, market, book, taker_fee, account tools); trading tools only
|
|
46
|
+
registered when the operator sets `PMQ_MCP_LIVE=1`, per-order cap via
|
|
47
|
+
`PMQ_MCP_MAX_USD`.
|
|
48
|
+
* `bot-template/`: market-agnostic bot engine (strategy owns `watchlist()`
|
|
49
|
+
and `decide()`), honest paper mode against real books, risk rails (budget
|
|
50
|
+
with fee headroom, poisoning, halts with exit code 42, disk-persisted
|
|
51
|
+
daily halt), systemd unit, phone dashboard.
|
pmquant-0.3.0/CLAUDE.md
ADDED
|
@@ -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.
|
|
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
|
|
@@ -19,12 +19,18 @@ Requires-Dist: py-clob-client-v2<2,>=1.0.2
|
|
|
19
19
|
Provides-Extra: dev
|
|
20
20
|
Requires-Dist: mcp>=1.2; extra == 'dev'
|
|
21
21
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
22
23
|
Provides-Extra: mcp
|
|
23
24
|
Requires-Dist: mcp>=1.2; extra == 'mcp'
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
25
26
|
|
|
26
27
|
# pmq
|
|
27
28
|
|
|
29
|
+
[](https://pypi.org/project/pmquant/)
|
|
30
|
+
[](https://github.com/crp4222/pmq/actions/workflows/test.yml)
|
|
31
|
+
[](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
|
|
28
34
|
Fail-closed execution and market data for **Polymarket CLOB V2**, in Python.
|
|
29
35
|
Local signing (your keys never leave your process), exchange-confirmed fills
|
|
30
36
|
only, fee-correct math, and deposit-wallet (`POLY_1271`) support that actually
|
|
@@ -63,6 +69,35 @@ a real error in live trading:
|
|
|
63
69
|
|
|
64
70
|
The full write-up with reproduction details: [docs/war-story.md](docs/war-story.md).
|
|
65
71
|
|
|
72
|
+
## Runs in production
|
|
73
|
+
|
|
74
|
+
The maintainer's own bot trades through this exact executor 24/7 with real
|
|
75
|
+
money. Example receipt (2026-07-03): settlement transaction
|
|
76
|
+
[`0x387f5f09...100d88a8`](https://polygonscan.com/tx/0x387f5f09c031bb36a71c54adc978b1ed4d50c67f6dd3f0c2c8068391100d88a8)
|
|
77
|
+
on the CTF Exchange V2: a FAK market buy built by this library, matched and
|
|
78
|
+
settled, with the builder code visible in the calldata. Additionally, a weekly
|
|
79
|
+
[canary workflow](.github/workflows/canary.yml) exercises the real endpoints
|
|
80
|
+
and the installed client surface, and opens an issue by itself if Polymarket
|
|
81
|
+
drifts.
|
|
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
|
+

|
|
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
|
+
|
|
66
101
|
## The contract: nothing is booked without exchange confirmation
|
|
67
102
|
|
|
68
103
|
| Situation | What pmq does |
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# pmq
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/pmquant/)
|
|
4
|
+
[](https://github.com/crp4222/pmq/actions/workflows/test.yml)
|
|
5
|
+
[](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
3
8
|
Fail-closed execution and market data for **Polymarket CLOB V2**, in Python.
|
|
4
9
|
Local signing (your keys never leave your process), exchange-confirmed fills
|
|
5
10
|
only, fee-correct math, and deposit-wallet (`POLY_1271`) support that actually
|
|
@@ -38,6 +43,35 @@ a real error in live trading:
|
|
|
38
43
|
|
|
39
44
|
The full write-up with reproduction details: [docs/war-story.md](docs/war-story.md).
|
|
40
45
|
|
|
46
|
+
## Runs in production
|
|
47
|
+
|
|
48
|
+
The maintainer's own bot trades through this exact executor 24/7 with real
|
|
49
|
+
money. Example receipt (2026-07-03): settlement transaction
|
|
50
|
+
[`0x387f5f09...100d88a8`](https://polygonscan.com/tx/0x387f5f09c031bb36a71c54adc978b1ed4d50c67f6dd3f0c2c8068391100d88a8)
|
|
51
|
+
on the CTF Exchange V2: a FAK market buy built by this library, matched and
|
|
52
|
+
settled, with the builder code visible in the calldata. Additionally, a weekly
|
|
53
|
+
[canary workflow](.github/workflows/canary.yml) exercises the real endpoints
|
|
54
|
+
and the installed client surface, and opens an issue by itself if Polymarket
|
|
55
|
+
drifts.
|
|
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
|
+

|
|
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
|
+
|
|
41
75
|
## The contract: nothing is booked without exchange confirmation
|
|
42
76
|
|
|
43
77
|
| Situation | What pmq does |
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Posture
|
|
4
|
+
|
|
5
|
+
* Your private key is read from the environment, used to instantiate the
|
|
6
|
+
local signer, and never logged, transmitted or stored by pmq. There is no
|
|
7
|
+
backend, no telemetry, no custody. The only hosts contacted are Polymarket
|
|
8
|
+
endpoints (clob/gamma/data-api) and, for the on-chain debug helpers you run
|
|
9
|
+
yourself, the RPC you choose.
|
|
10
|
+
* The builder code embedded by default is attribution metadata inside the
|
|
11
|
+
signed order (public on-chain either way). It carries 0/0 commission and is
|
|
12
|
+
disabled with `builder_code=None`. It cannot access funds.
|
|
13
|
+
* The executor refuses to trade if the installed py-clob-client-v2 no longer
|
|
14
|
+
matches the API surface pmq was verified against (introspection at startup),
|
|
15
|
+
rather than signing through changed semantics.
|
|
16
|
+
|
|
17
|
+
## Reading the source before trusting it
|
|
18
|
+
|
|
19
|
+
A documented wave of fake "polymarket bot" repositories steals private keys.
|
|
20
|
+
pmq is deliberately small (five modules) so you can audit the entire execution
|
|
21
|
+
path in minutes. Grep targets that settle the important questions fast:
|
|
22
|
+
`POLY_PRIVATE_KEY` (read once, passed to the official client), `builder_code`
|
|
23
|
+
(the disclosure and the opt-out), `http` (every host contacted).
|
|
24
|
+
|
|
25
|
+
## Reporting a vulnerability
|
|
26
|
+
|
|
27
|
+
Open a GitHub security advisory on this repository (Security tab, "Report a
|
|
28
|
+
vulnerability") or an issue with the `security` label if it is not sensitive.
|
|
29
|
+
You will get an answer within a few days.
|
|
@@ -86,6 +86,39 @@ def write_halt_flag(utc_day, pnl):
|
|
|
86
86
|
log(f"halt flag write failed ({e}); halt still enforced in-process")
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
def recover_orphans(tracked):
|
|
90
|
+
"""Markets that got a fill but no scoring row (a restart between fill and
|
|
91
|
+
resolution wipes in-memory state) are rebuilt so the scoring loop settles
|
|
92
|
+
them. Live scoring re-pulls exchange truth, so numbers cannot drift."""
|
|
93
|
+
try:
|
|
94
|
+
scored = set()
|
|
95
|
+
if os.path.exists(WINDOWS_CSV):
|
|
96
|
+
for r in csv.DictReader(open(WINDOWS_CSV)):
|
|
97
|
+
scored.add((r["family"], r["mode"]))
|
|
98
|
+
if not os.path.exists(FILLS_CSV):
|
|
99
|
+
return
|
|
100
|
+
now, orphans = time.time(), {}
|
|
101
|
+
for r in csv.DictReader(open(FILLS_CSV)):
|
|
102
|
+
if r["mode"] != MODE or (r["family"], MODE) in scored:
|
|
103
|
+
continue
|
|
104
|
+
o = orphans.setdefault(r["family"], {"side": r["side"], "fills": []})
|
|
105
|
+
o["fills"].append((float(r["price"]), float(r["shares"])))
|
|
106
|
+
for slug, o in orphans.items():
|
|
107
|
+
pm = pmq.parse_market(pmq.get_market(slug, log))
|
|
108
|
+
if not pm:
|
|
109
|
+
log(f"orphan {slug}: market unresolvable, skipped")
|
|
110
|
+
continue
|
|
111
|
+
spent = sum(p * s for p, s in o["fills"])
|
|
112
|
+
fees = sum(pmq.fee(p, s, FEE_RATE) for p, s in o["fills"])
|
|
113
|
+
tracked[slug] = {"pm": pm, "first_seen": now - MAX_TRACK_H * 1800,
|
|
114
|
+
"fills": o["fills"], "spent": spent, "fees": fees,
|
|
115
|
+
"side": o["side"], "winner": None, "src": "",
|
|
116
|
+
"resolved": False, "poisoned": False, "sstate": {}}
|
|
117
|
+
log(f"orphan recovered: {slug} ({len(o['fills'])} fill(s)), will be scored")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
log(f"orphan recovery failed (continuing without): {e}")
|
|
120
|
+
|
|
121
|
+
|
|
89
122
|
def main(run_hours):
|
|
90
123
|
t_end = time.time() + run_hours * 3600
|
|
91
124
|
ex = None
|
|
@@ -109,6 +142,7 @@ def main(run_hours):
|
|
|
109
142
|
f"daily_halt={DAILY_HALT_USD}$ strategy={strategy.NAME}")
|
|
110
143
|
|
|
111
144
|
tracked, day_pnl, halted_day = {}, {}, None
|
|
145
|
+
recover_orphans(tracked)
|
|
112
146
|
last_score_poll = 0.0
|
|
113
147
|
|
|
114
148
|
while time.time() < t_end:
|
|
@@ -251,6 +285,12 @@ def main(run_hours):
|
|
|
251
285
|
log(f"{slug}: SCORED {st['side']} net={net:+.2f}$ "
|
|
252
286
|
f"winner={winner} day={day_pnl[d]:+.2f}$")
|
|
253
287
|
st["resolved"] = True
|
|
288
|
+
if ex:
|
|
289
|
+
# republish the CLOB-visible balance so the dashboard
|
|
290
|
+
# tracks exchange truth without ever holding keys
|
|
291
|
+
c = ex.collateral()
|
|
292
|
+
if c > 0:
|
|
293
|
+
log(f"collateral {c:.2f} USDC")
|
|
254
294
|
|
|
255
295
|
except SystemExit:
|
|
256
296
|
raise
|
|
@@ -94,7 +94,8 @@ def service_state():
|
|
|
94
94
|
"-n", "1500", "--no-pager", "-q"])
|
|
95
95
|
lines = jout.splitlines()
|
|
96
96
|
for line in reversed(lines):
|
|
97
|
-
|
|
97
|
+
# matches both the startup line and the per-scoring republication
|
|
98
|
+
if "collateral" in line:
|
|
98
99
|
try:
|
|
99
100
|
collateral = float(line.split("collateral", 1)[1].split()[0])
|
|
100
101
|
collateral_ts = float(line.split(" ", 1)[0])
|
|
@@ -97,7 +97,7 @@ footer .stale{color:var(--warn);font-weight:600}
|
|
|
97
97
|
</div>
|
|
98
98
|
|
|
99
99
|
<div class="tiles">
|
|
100
|
-
<div class="tile"><div class="lbl">Balance
|
|
100
|
+
<div class="tile"><div class="lbl">Balance</div><div class="val" id="tBal">…</div><div class="hint" id="tBalH"></div></div>
|
|
101
101
|
<div class="tile"><div class="lbl">Win rate</div><div class="val" id="tWr">…</div><div class="hint" id="tWrH"></div></div>
|
|
102
102
|
<div class="tile"><div class="lbl">PnL période</div><div class="val" id="tPnl">…</div><div class="hint" id="tPnlH"></div></div>
|
|
103
103
|
<div class="tile"><div class="lbl">Frais payés</div><div class="val" id="tFee">…</div><div class="hint" id="tFeeH"></div></div>
|
|
@@ -186,7 +186,9 @@ function renderTiles(){
|
|
|
186
186
|
const extra = d.windows.filter(w => w.mode === "live" && (w.scored_ts||0) > (s.collateral_ts||0))
|
|
187
187
|
.reduce((a,w) => a + (w.net_pnl||0), 0);
|
|
188
188
|
$("tBal").textContent = "~" + (s.collateral + extra).toFixed(2) + "$";
|
|
189
|
-
$("tBalH").textContent = "
|
|
189
|
+
$("tBalH").textContent = "vue exchange, maj il y a "
|
|
190
|
+
+ (s.collateral_ts ? ago(d.now - s.collateral_ts) : "?")
|
|
191
|
+
+ (extra ? " + scoré depuis" : "");
|
|
190
192
|
} else { $("tBal").textContent = "?"; $("tBalH").textContent = "visible en mode live"; }
|
|
191
193
|
$("tWr").textContent = rows.length ? Math.round(100*wins/rows.length) + "%" : "…";
|
|
192
194
|
$("tWrH").textContent = rows.length ? wins + "W/" + (rows.length-wins) + "L · " + state.mode + " " + per : "aucune fenêtre";
|
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# CLOB V2 order rounding, measured
|
|
2
|
+
|
|
3
|
+
*Controlled experiment against production, 2026-07-03, py-clob-client-v2
|
|
4
|
+
1.0.2, total risk budget 10 USD (net result of the run: +0.39). Raw data
|
|
5
|
+
posted on the client's issues
|
|
6
|
+
[#89](https://github.com/Polymarket/py-clob-client-v2/issues/89#issuecomment-4875871905)
|
|
7
|
+
and [#66](https://github.com/Polymarket/py-clob-client-v2/issues/66#issuecomment-4875871996).*
|
|
8
|
+
|
|
9
|
+
## Method
|
|
10
|
+
|
|
11
|
+
Phase A: resting GTC bids far below the touch (cannot fill, cancelled after
|
|
12
|
+
each case, zero cost), submitted with adversarial sizes and prices, then read
|
|
13
|
+
back through `get_open_orders` to see what the server actually recorded.
|
|
14
|
+
Phase B: two marketable limit buys on a liquid market to observe matched
|
|
15
|
+
amounts against requested size, cross-checked with `get_trades`.
|
|
16
|
+
|
|
17
|
+
## Results
|
|
18
|
+
|
|
19
|
+
| Case | Requested | Server-side view |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Baseline 2-decimal size | 5.25 @ 0.05 | 5.25, exact |
|
|
22
|
+
| 4-decimal size | 5.2537 | **5.25, silently rounded down** |
|
|
23
|
+
| Float drift | 5.100000000000001 | 5.1, normalized |
|
|
24
|
+
| Price finer than tick | 0.0515 (tick 0.01) | **0.05, silently rounded down** |
|
|
25
|
+
| Sub-cent notional | 5.007 @ 0.03 | accepted, size recorded as 5.00 |
|
|
26
|
+
| Below minimum size | size 4 (min 5) | HTTP 400: `Size (4) lower than the minimum: 5` |
|
|
27
|
+
| Marketable fill, odd size | 5.1234 @ 0.96 | signed as 5.12; matched **exactly 5.12** shares for 4.9152 USDC (0.96 x 5.12 to the cent) |
|
|
28
|
+
| Marketable fill, control | 5.0 @ 0.96 | matched exactly 5.00 for 4.80 USDC |
|
|
29
|
+
|
|
30
|
+
## What this means
|
|
31
|
+
|
|
32
|
+
1. On 1.0.2, every input is normalized CLIENT-side (rounded DOWN to the
|
|
33
|
+
allowed decimals per the tick's RoundConfig) before signing. Float-drift
|
|
34
|
+
artifacts never reach the wire.
|
|
35
|
+
2. The normalization is **silent**. If your own accounting keeps the
|
|
36
|
+
unrounded size or price, it diverges from what the exchange signed. Book
|
|
37
|
+
from the response's matched amounts (what pmq's `Fill` does), never from
|
|
38
|
+
your request.
|
|
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.
|
|
50
|
+
4. The per-market minimum size is enforced server-side with a clean 400, and
|
|
51
|
+
is readable in advance from the book response (`min_order_size`, exposed
|
|
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.
|
|
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" }
|
|
@@ -31,10 +31,19 @@ Issues = "https://github.com/crp4222/pmq/issues"
|
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
33
|
mcp = ["mcp>=1.2"]
|
|
34
|
-
dev = ["pytest>=8", "mcp>=1.2"]
|
|
34
|
+
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"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 100
|
|
45
|
+
target-version = "py310"
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "W", "I"]
|
|
49
|
+
ignore = ["E501"]
|