pmquant 0.1.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.1.0/.github/workflows/publish.yml +33 -0
- pmquant-0.1.0/.github/workflows/test.yml +17 -0
- pmquant-0.1.0/.gitignore +8 -0
- pmquant-0.1.0/AGENTS.md +40 -0
- pmquant-0.1.0/CHANGELOG.md +25 -0
- pmquant-0.1.0/LICENSE +21 -0
- pmquant-0.1.0/PKG-INFO +211 -0
- pmquant-0.1.0/README.md +186 -0
- pmquant-0.1.0/bot-template/README.md +65 -0
- pmquant-0.1.0/bot-template/bot.py +265 -0
- pmquant-0.1.0/bot-template/dash/bot_dash.py +175 -0
- pmquant-0.1.0/bot-template/dash/dash.html +406 -0
- pmquant-0.1.0/bot-template/pmq-bot.service +26 -0
- pmquant-0.1.0/bot-template/strategy.py +50 -0
- pmquant-0.1.0/docs/war-story.md +140 -0
- pmquant-0.1.0/examples/fak_buy_guarded.py +29 -0
- pmquant-0.1.0/examples/read_market.py +20 -0
- pmquant-0.1.0/llms.txt +45 -0
- pmquant-0.1.0/pyproject.toml +40 -0
- pmquant-0.1.0/src/pmq/__init__.py +31 -0
- pmquant-0.1.0/src/pmq/data.py +208 -0
- pmquant-0.1.0/src/pmq/exceptions.py +19 -0
- pmquant-0.1.0/src/pmq/executor.py +329 -0
- pmquant-0.1.0/src/pmq/mcp.py +188 -0
- pmquant-0.1.0/tests/test_data.py +80 -0
- pmquant-0.1.0/tests/test_executor.py +162 -0
- pmquant-0.1.0/tests/test_mcp.py +38 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install
|
|
22
|
+
run: pip install -e ".[dev]"
|
|
23
|
+
|
|
24
|
+
- name: Test
|
|
25
|
+
run: python -m pytest tests/ -q
|
|
26
|
+
|
|
27
|
+
- name: Build
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade build
|
|
30
|
+
python -m build
|
|
31
|
+
|
|
32
|
+
- name: Publish to PyPI
|
|
33
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
pull_request:
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
strategy:
|
|
9
|
+
matrix:
|
|
10
|
+
python-version: ["3.10", "3.12"]
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: ${{ matrix.python-version }}
|
|
16
|
+
- run: pip install -e ".[dev]"
|
|
17
|
+
- run: pytest -q
|
pmquant-0.1.0/.gitignore
ADDED
pmquant-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Working with pmq (agent instructions)
|
|
2
|
+
|
|
3
|
+
pmq is a small Python library for Polymarket CLOB V2 with two layers.
|
|
4
|
+
|
|
5
|
+
## Data layer (no credentials, safe anywhere)
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import pmq
|
|
9
|
+
m = pmq.parse_market(pmq.get_market(slug)) # condition id + token ids
|
|
10
|
+
book = pmq.get_book(m["token_a"]) # REAL TIME, trade off this
|
|
11
|
+
bid, bid_sz, ask, ask_sz = pmq.best_bid_ask(book)
|
|
12
|
+
pmq.fee(price, shares) # taker $, crypto rate 0.07
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Never make live decisions from `get_tape` (data-api): it lags matching by 1
|
|
16
|
+
to 3 minutes. It is for offline scoring at least 5 minutes after close.
|
|
17
|
+
|
|
18
|
+
## Execution layer (needs POLY_PRIVATE_KEY; treat as production)
|
|
19
|
+
|
|
20
|
+
Rules an agent MUST follow:
|
|
21
|
+
|
|
22
|
+
1. Default to reading data, not trading. Only place orders when the user
|
|
23
|
+
explicitly asked for live execution in this session.
|
|
24
|
+
2. Book only `fill.matched_shares` and `fill.matched_usd`. A falsy `Fill`
|
|
25
|
+
means nothing happened, whatever the request was.
|
|
26
|
+
3. Wrap every order in `try/except OrderUncertain`; on that exception call
|
|
27
|
+
`executor.reconcile(condition_id)` and do not place new orders on that
|
|
28
|
+
market until it returns.
|
|
29
|
+
4. Call `executor.require_collateral(stake)` before a session of trading.
|
|
30
|
+
If it reports 0 while the user says the account is funded, the
|
|
31
|
+
signature_type is wrong: the Polymarket app's default wallet needs
|
|
32
|
+
`signature_type=3` (deposit wallet).
|
|
33
|
+
5. Budget with fees: a share at price p costs `p + pmq.fee(p, 1)`.
|
|
34
|
+
6. Never print or log POLY_PRIVATE_KEY or any environment secret. Addresses
|
|
35
|
+
and builder codes are public; keys are not.
|
|
36
|
+
7. FAK buys spend dollars (`buy_fak(token, price_cap, usd)`), FAK sells
|
|
37
|
+
spend shares. Amounts are rounded down internally to exchange accuracy.
|
|
38
|
+
8. Builder attribution: the library defaults to the maintainer's
|
|
39
|
+
zero-commission builder code. Respect the user's choice if they set
|
|
40
|
+
`builder_code=None` or their own code; do not change it silently.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-07-03)
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
* `pmq.data`: real-time books (with per-market exchange rules via
|
|
8
|
+
`book_meta`), gamma slug resolution with expired-market fallback,
|
|
9
|
+
market-agnostic `parse_market` (any binary outcomes, close time), settled
|
|
10
|
+
and book-inferred winners, offline trade tape, official per-category taker
|
|
11
|
+
fee formula.
|
|
12
|
+
* `pmq.executor.PolymarketExecutor`: fail-closed CLOB V2 execution. FAK
|
|
13
|
+
buys/sells through the market-order path, exchange-confirmed fills only,
|
|
14
|
+
`OrderUncertain` + `reconcile()` from get_trades, deposit-wallet
|
|
15
|
+
(POLY_1271) support, collateral fail-fast, builder-code default with
|
|
16
|
+
disclosure and opt-out, startup introspection of the installed
|
|
17
|
+
py-clob-client-v2.
|
|
18
|
+
* `pmq.mcp` (`pmq-mcp`): MCP server. Read tools always available
|
|
19
|
+
(find_markets, market, book, taker_fee, account tools); trading tools only
|
|
20
|
+
registered when the operator sets `PMQ_MCP_LIVE=1`, per-order cap via
|
|
21
|
+
`PMQ_MCP_MAX_USD`.
|
|
22
|
+
* `bot-template/`: market-agnostic bot engine (strategy owns `watchlist()`
|
|
23
|
+
and `decide()`), honest paper mode against real books, risk rails (budget
|
|
24
|
+
with fee headroom, poisoning, halts with exit code 42, disk-persisted
|
|
25
|
+
daily halt), systemd unit, phone dashboard.
|
pmquant-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 crp4222
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pmquant-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pmquant
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
Project-URL: Homepage, https://github.com/crp4222/pmq
|
|
6
|
+
Project-URL: Issues, https://github.com/crp4222/pmq/issues
|
|
7
|
+
Author: crp4222
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: builder-code,clob,clob-v2,deposit-wallet,execution,fak,market-order,orderbook,poly-1271,polymarket,prediction-markets,quant,trading
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: py-clob-client-v2<2,>=1.0.2
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: mcp>=1.2; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
22
|
+
Provides-Extra: mcp
|
|
23
|
+
Requires-Dist: mcp>=1.2; extra == 'mcp'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# pmq
|
|
27
|
+
|
|
28
|
+
Fail-closed execution and market data for **Polymarket CLOB V2**, in Python.
|
|
29
|
+
Local signing (your keys never leave your process), exchange-confirmed fills
|
|
30
|
+
only, fee-correct math, and deposit-wallet (`POLY_1271`) support that actually
|
|
31
|
+
works in production.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install pmquant # distribution name pmquant, import name pmq
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
(PyPI's similarity check reserves the bare name; the module you import is
|
|
38
|
+
`pmq`, same pattern as beautifulsoup4/bs4.)
|
|
39
|
+
|
|
40
|
+
As of 2026-07-03 this is, to our knowledge, the **only maintained Python
|
|
41
|
+
layer combining local CLOB V2 signing, an exchange-confirmed fill contract,
|
|
42
|
+
and working deposit-wallet (POLY_1271) auth**. That claim is dated and
|
|
43
|
+
falsifiable: the comparison table below names the alternatives and what each
|
|
44
|
+
does instead; open an issue if it goes stale.
|
|
45
|
+
|
|
46
|
+
## Why this exists
|
|
47
|
+
|
|
48
|
+
Polymarket cut over to CLOB V2 on 2026-04-28. V1-signed orders are rejected in
|
|
49
|
+
production, the fee schedule is decided at match time, and the official client
|
|
50
|
+
examples leave several traps undocumented. Every line of pmq was paid for with
|
|
51
|
+
a real error in live trading:
|
|
52
|
+
|
|
53
|
+
* `invalid amounts, the market buy orders maker amount supports a max accuracy
|
|
54
|
+
of 2 decimals`: the CLOB treats FAK/FOK buys as **market orders**. pmq routes
|
|
55
|
+
them through the market-order builder with the correct rounding.
|
|
56
|
+
* `no orders found to match with FAK order` (HTTP 400, yet with an `orderID`):
|
|
57
|
+
a clean no-fill, not an error. pmq returns an empty `Fill` instead of crashing
|
|
58
|
+
or, worse, retrying blindly.
|
|
59
|
+
* CLOB shows `balance: 0` while your pUSD sits on-chain: the balance endpoint
|
|
60
|
+
ignores your `funder` parameter and derives the wallet from your EOA and
|
|
61
|
+
`signature_type`. Funds in the Polymarket app's default wallet (an ERC-1271
|
|
62
|
+
deposit wallet) are only visible with `signature_type=3`.
|
|
63
|
+
|
|
64
|
+
The full write-up with reproduction details: [docs/war-story.md](docs/war-story.md).
|
|
65
|
+
|
|
66
|
+
## The contract: nothing is booked without exchange confirmation
|
|
67
|
+
|
|
68
|
+
| Situation | What pmq does |
|
|
69
|
+
|---|---|
|
|
70
|
+
| Response is a dict with `orderID`, not flagged failed | `Fill` with the **matched** size read from the response |
|
|
71
|
+
| Error dict on HTTP 200, string body, `success: false` | `Fill(rejected=True)`, zero booked |
|
|
72
|
+
| HTTP 4xx (incl. FAK no-match) | `Fill(rejected=True)`, zero booked |
|
|
73
|
+
| Timeout, 5xx, exception after send | raises `OrderUncertain`: the order MAY exist. Call `reconcile()` before trading that market again |
|
|
74
|
+
| Unparseable matched amounts | zero booked (fail closed) |
|
|
75
|
+
|
|
76
|
+
`reconcile(condition_id)` cancels anything resting, verifies nothing stayed
|
|
77
|
+
open, and returns `(shares, usd, fees)` from `get_trades`: the exchange truth,
|
|
78
|
+
not your hopes.
|
|
79
|
+
|
|
80
|
+
At startup pmq **introspects the installed py-clob-client-v2** against the API
|
|
81
|
+
surface it was verified on, and refuses to trade on drift instead of sending
|
|
82
|
+
orders through changed semantics.
|
|
83
|
+
|
|
84
|
+
## Quickstart
|
|
85
|
+
|
|
86
|
+
Market data needs no keys:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import pmq
|
|
90
|
+
|
|
91
|
+
m = pmq.parse_market(pmq.get_market("btc-updown-15m-1783062000"))
|
|
92
|
+
book = pmq.get_book(m["token_a"])
|
|
93
|
+
bid, bid_sz, ask, ask_sz = pmq.best_bid_ask(book)
|
|
94
|
+
print(ask, pmq.band_ask_depth_usd(book, 0.90, 0.97))
|
|
95
|
+
print(pmq.fee(price=0.95, shares=100)) # taker fee in $, crypto rate
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Execution (reads `POLY_PRIVATE_KEY`, `POLY_FUNDER`, `POLY_SIG_TYPE` from the
|
|
99
|
+
environment):
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from pmq import PolymarketExecutor, OrderUncertain
|
|
103
|
+
|
|
104
|
+
ex = PolymarketExecutor() # signature_type=3 for the app's deposit wallet
|
|
105
|
+
ex.require_collateral(5.0) # fail fast, with a diagnostic that names sig_type
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
fill = ex.buy_fak(token_id=m["token_a"], price_cap=0.95, usd=5.00)
|
|
109
|
+
except OrderUncertain:
|
|
110
|
+
ex.reconcile(m["condition_id"], m["token_a"]) # exchange truth before anything else
|
|
111
|
+
else:
|
|
112
|
+
if fill: # book ONLY what matched
|
|
113
|
+
print(fill.matched_shares, "shares at", fill.price, "order", fill.order_id)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`sell_fak` and `limit_gtc` follow the same contract. The buy path has carried
|
|
117
|
+
live volume; treat the sell path as following the same documented semantics
|
|
118
|
+
with less battle time.
|
|
119
|
+
|
|
120
|
+
## The signature_type table nobody gives you
|
|
121
|
+
|
|
122
|
+
| `signature_type` | Wallet | When it is yours |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| 0 | the EOA itself | you trade from a bare private key |
|
|
125
|
+
| 1 | `POLY_PROXY` | email/Magic accounts (legacy) |
|
|
126
|
+
| 2 | `POLY_GNOSIS_SAFE` | browser-wallet proxy |
|
|
127
|
+
| 3 | `POLY_1271` deposit wallet | **the Polymarket app's default wallet** |
|
|
128
|
+
|
|
129
|
+
If `collateral()` returns 0 while the funds are visible on-chain on your funder
|
|
130
|
+
address, your `signature_type` is wrong. Debug trick: `eth_call` `owner()`
|
|
131
|
+
(`0x8da5cb5b`) on the funder; if it returns your EOA and the wallet bytecode is
|
|
132
|
+
an ERC-1167 proxy, you want `signature_type=3`.
|
|
133
|
+
|
|
134
|
+
## Comparison (2026-07-03, factual)
|
|
135
|
+
|
|
136
|
+
| | pmq | py-clob-client-v2 (official) | pmxt | NautilusTrader | caiovicentino MCP |
|
|
137
|
+
|---|---|---|---|---|---|
|
|
138
|
+
| CLOB V2 signing | yes, local | yes, local | writes via its hosted backend | yes, local | V1 only (rejected in prod since 2026-04-28) |
|
|
139
|
+
| Confirmed-fill contract | yes (core design) | no (raw responses) | n/a | engine-level | no |
|
|
140
|
+
| Deposit wallet / POLY_1271 | yes, production-proven | open issues (#70 and others) | n/a | untested claim | no |
|
|
141
|
+
| Fee math | official per-category formula | fee at match, no helper | via backend | fee model | fee-blind |
|
|
142
|
+
| Reconciliation helper | yes | no | n/a | engine-level | no |
|
|
143
|
+
| Footprint | one small lib | one small lib | multi-venue platform | full trading framework | MCP server |
|
|
144
|
+
|
|
145
|
+
NautilusTrader is excellent if you want a full framework; pmq is the small
|
|
146
|
+
library you embed in your own bot. pmxt is convenient if you accept routing
|
|
147
|
+
writes through their backend; pmq exists for self-custody.
|
|
148
|
+
|
|
149
|
+
## Builder code disclosure
|
|
150
|
+
|
|
151
|
+
pmq ships with the maintainer's public Polymarket **builder code** as default
|
|
152
|
+
attribution inside signed orders (`pmq.executor.DEFAULT_BUILDER_CODE`). Its
|
|
153
|
+
commission is set to **0/0: it never adds any fee to your orders**. Attribution
|
|
154
|
+
feeds Polymarket's builder program and funds this project at zero cost to you.
|
|
155
|
+
|
|
156
|
+
Opt out or replace it, one line either way:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
PolymarketExecutor(builder_code=None) # no attribution
|
|
160
|
+
PolymarketExecutor(builder_code="0xYOURS...") # your own code
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
or set the `POLY_BUILDER_CODE` environment variable. (Same model as
|
|
164
|
+
JKorf/Polymarket.Net; the official client defaults to zero attribution.)
|
|
165
|
+
|
|
166
|
+
## MCP server (agents)
|
|
167
|
+
|
|
168
|
+
`pip install "pmquant[mcp]"` then run `pmq-mcp` (stdio). Read tools (market,
|
|
169
|
+
book, taker_fee, account_collateral, account_trades) always exist. Trading
|
|
170
|
+
tools (`fak_buy`, `fak_sell`, `cancel_and_reconcile`) are **only registered
|
|
171
|
+
when the operator sets `PMQ_MCP_LIVE=1`** in the server environment: an
|
|
172
|
+
agent cannot talk its way past a tool that was never created. Every order is
|
|
173
|
+
capped per call by `PMQ_MCP_MAX_USD` (default 10).
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"mcpServers": {
|
|
178
|
+
"pmq": {
|
|
179
|
+
"command": "pmq-mcp",
|
|
180
|
+
"env": { "POLY_PRIVATE_KEY": "...", "POLY_FUNDER": "0x...", "POLY_SIG_TYPE": "3" }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Leave the `POLY_*` variables out entirely for a read-only market-data server.
|
|
187
|
+
|
|
188
|
+
## Bot template
|
|
189
|
+
|
|
190
|
+
[bot-template/](bot-template/) is a complete bot minus the strategy, for ANY
|
|
191
|
+
market (politics, sports, crypto, culture): paper mode against real books
|
|
192
|
+
with real fees, per-market budgets with fee headroom, poisoned-market
|
|
193
|
+
reconciliation, consecutive-failure halt, disk-persisted daily loss halt, a
|
|
194
|
+
systemd unit with `RestartPreventExitStatus=42` so halts stay halted, and a
|
|
195
|
+
lightweight phone dashboard. You implement `watchlist()` and `decide()`; the
|
|
196
|
+
shipped demo strategy is an API illustration meant to be replaced.
|
|
197
|
+
|
|
198
|
+
## Security posture
|
|
199
|
+
|
|
200
|
+
* Keys are read from the environment, used to instantiate the signer, and
|
|
201
|
+
never logged. No custody, no backend, no telemetry, zero network calls
|
|
202
|
+
besides Polymarket endpoints.
|
|
203
|
+
* Beware of the documented wave of fake "polymarket bot" repositories that
|
|
204
|
+
steal private keys. Read the source: pmq is small on purpose.
|
|
205
|
+
* Fund the trading wallet with what you can afford to lose. Nothing here is
|
|
206
|
+
financial advice; prediction-market access is restricted in some
|
|
207
|
+
jurisdictions and compliance is on you.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
pmquant-0.1.0/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# pmq
|
|
2
|
+
|
|
3
|
+
Fail-closed execution and market data for **Polymarket CLOB V2**, in Python.
|
|
4
|
+
Local signing (your keys never leave your process), exchange-confirmed fills
|
|
5
|
+
only, fee-correct math, and deposit-wallet (`POLY_1271`) support that actually
|
|
6
|
+
works in production.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install pmquant # distribution name pmquant, import name pmq
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
(PyPI's similarity check reserves the bare name; the module you import is
|
|
13
|
+
`pmq`, same pattern as beautifulsoup4/bs4.)
|
|
14
|
+
|
|
15
|
+
As of 2026-07-03 this is, to our knowledge, the **only maintained Python
|
|
16
|
+
layer combining local CLOB V2 signing, an exchange-confirmed fill contract,
|
|
17
|
+
and working deposit-wallet (POLY_1271) auth**. That claim is dated and
|
|
18
|
+
falsifiable: the comparison table below names the alternatives and what each
|
|
19
|
+
does instead; open an issue if it goes stale.
|
|
20
|
+
|
|
21
|
+
## Why this exists
|
|
22
|
+
|
|
23
|
+
Polymarket cut over to CLOB V2 on 2026-04-28. V1-signed orders are rejected in
|
|
24
|
+
production, the fee schedule is decided at match time, and the official client
|
|
25
|
+
examples leave several traps undocumented. Every line of pmq was paid for with
|
|
26
|
+
a real error in live trading:
|
|
27
|
+
|
|
28
|
+
* `invalid amounts, the market buy orders maker amount supports a max accuracy
|
|
29
|
+
of 2 decimals`: the CLOB treats FAK/FOK buys as **market orders**. pmq routes
|
|
30
|
+
them through the market-order builder with the correct rounding.
|
|
31
|
+
* `no orders found to match with FAK order` (HTTP 400, yet with an `orderID`):
|
|
32
|
+
a clean no-fill, not an error. pmq returns an empty `Fill` instead of crashing
|
|
33
|
+
or, worse, retrying blindly.
|
|
34
|
+
* CLOB shows `balance: 0` while your pUSD sits on-chain: the balance endpoint
|
|
35
|
+
ignores your `funder` parameter and derives the wallet from your EOA and
|
|
36
|
+
`signature_type`. Funds in the Polymarket app's default wallet (an ERC-1271
|
|
37
|
+
deposit wallet) are only visible with `signature_type=3`.
|
|
38
|
+
|
|
39
|
+
The full write-up with reproduction details: [docs/war-story.md](docs/war-story.md).
|
|
40
|
+
|
|
41
|
+
## The contract: nothing is booked without exchange confirmation
|
|
42
|
+
|
|
43
|
+
| Situation | What pmq does |
|
|
44
|
+
|---|---|
|
|
45
|
+
| Response is a dict with `orderID`, not flagged failed | `Fill` with the **matched** size read from the response |
|
|
46
|
+
| Error dict on HTTP 200, string body, `success: false` | `Fill(rejected=True)`, zero booked |
|
|
47
|
+
| HTTP 4xx (incl. FAK no-match) | `Fill(rejected=True)`, zero booked |
|
|
48
|
+
| Timeout, 5xx, exception after send | raises `OrderUncertain`: the order MAY exist. Call `reconcile()` before trading that market again |
|
|
49
|
+
| Unparseable matched amounts | zero booked (fail closed) |
|
|
50
|
+
|
|
51
|
+
`reconcile(condition_id)` cancels anything resting, verifies nothing stayed
|
|
52
|
+
open, and returns `(shares, usd, fees)` from `get_trades`: the exchange truth,
|
|
53
|
+
not your hopes.
|
|
54
|
+
|
|
55
|
+
At startup pmq **introspects the installed py-clob-client-v2** against the API
|
|
56
|
+
surface it was verified on, and refuses to trade on drift instead of sending
|
|
57
|
+
orders through changed semantics.
|
|
58
|
+
|
|
59
|
+
## Quickstart
|
|
60
|
+
|
|
61
|
+
Market data needs no keys:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import pmq
|
|
65
|
+
|
|
66
|
+
m = pmq.parse_market(pmq.get_market("btc-updown-15m-1783062000"))
|
|
67
|
+
book = pmq.get_book(m["token_a"])
|
|
68
|
+
bid, bid_sz, ask, ask_sz = pmq.best_bid_ask(book)
|
|
69
|
+
print(ask, pmq.band_ask_depth_usd(book, 0.90, 0.97))
|
|
70
|
+
print(pmq.fee(price=0.95, shares=100)) # taker fee in $, crypto rate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Execution (reads `POLY_PRIVATE_KEY`, `POLY_FUNDER`, `POLY_SIG_TYPE` from the
|
|
74
|
+
environment):
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from pmq import PolymarketExecutor, OrderUncertain
|
|
78
|
+
|
|
79
|
+
ex = PolymarketExecutor() # signature_type=3 for the app's deposit wallet
|
|
80
|
+
ex.require_collateral(5.0) # fail fast, with a diagnostic that names sig_type
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
fill = ex.buy_fak(token_id=m["token_a"], price_cap=0.95, usd=5.00)
|
|
84
|
+
except OrderUncertain:
|
|
85
|
+
ex.reconcile(m["condition_id"], m["token_a"]) # exchange truth before anything else
|
|
86
|
+
else:
|
|
87
|
+
if fill: # book ONLY what matched
|
|
88
|
+
print(fill.matched_shares, "shares at", fill.price, "order", fill.order_id)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`sell_fak` and `limit_gtc` follow the same contract. The buy path has carried
|
|
92
|
+
live volume; treat the sell path as following the same documented semantics
|
|
93
|
+
with less battle time.
|
|
94
|
+
|
|
95
|
+
## The signature_type table nobody gives you
|
|
96
|
+
|
|
97
|
+
| `signature_type` | Wallet | When it is yours |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| 0 | the EOA itself | you trade from a bare private key |
|
|
100
|
+
| 1 | `POLY_PROXY` | email/Magic accounts (legacy) |
|
|
101
|
+
| 2 | `POLY_GNOSIS_SAFE` | browser-wallet proxy |
|
|
102
|
+
| 3 | `POLY_1271` deposit wallet | **the Polymarket app's default wallet** |
|
|
103
|
+
|
|
104
|
+
If `collateral()` returns 0 while the funds are visible on-chain on your funder
|
|
105
|
+
address, your `signature_type` is wrong. Debug trick: `eth_call` `owner()`
|
|
106
|
+
(`0x8da5cb5b`) on the funder; if it returns your EOA and the wallet bytecode is
|
|
107
|
+
an ERC-1167 proxy, you want `signature_type=3`.
|
|
108
|
+
|
|
109
|
+
## Comparison (2026-07-03, factual)
|
|
110
|
+
|
|
111
|
+
| | pmq | py-clob-client-v2 (official) | pmxt | NautilusTrader | caiovicentino MCP |
|
|
112
|
+
|---|---|---|---|---|---|
|
|
113
|
+
| CLOB V2 signing | yes, local | yes, local | writes via its hosted backend | yes, local | V1 only (rejected in prod since 2026-04-28) |
|
|
114
|
+
| Confirmed-fill contract | yes (core design) | no (raw responses) | n/a | engine-level | no |
|
|
115
|
+
| Deposit wallet / POLY_1271 | yes, production-proven | open issues (#70 and others) | n/a | untested claim | no |
|
|
116
|
+
| Fee math | official per-category formula | fee at match, no helper | via backend | fee model | fee-blind |
|
|
117
|
+
| Reconciliation helper | yes | no | n/a | engine-level | no |
|
|
118
|
+
| Footprint | one small lib | one small lib | multi-venue platform | full trading framework | MCP server |
|
|
119
|
+
|
|
120
|
+
NautilusTrader is excellent if you want a full framework; pmq is the small
|
|
121
|
+
library you embed in your own bot. pmxt is convenient if you accept routing
|
|
122
|
+
writes through their backend; pmq exists for self-custody.
|
|
123
|
+
|
|
124
|
+
## Builder code disclosure
|
|
125
|
+
|
|
126
|
+
pmq ships with the maintainer's public Polymarket **builder code** as default
|
|
127
|
+
attribution inside signed orders (`pmq.executor.DEFAULT_BUILDER_CODE`). Its
|
|
128
|
+
commission is set to **0/0: it never adds any fee to your orders**. Attribution
|
|
129
|
+
feeds Polymarket's builder program and funds this project at zero cost to you.
|
|
130
|
+
|
|
131
|
+
Opt out or replace it, one line either way:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
PolymarketExecutor(builder_code=None) # no attribution
|
|
135
|
+
PolymarketExecutor(builder_code="0xYOURS...") # your own code
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
or set the `POLY_BUILDER_CODE` environment variable. (Same model as
|
|
139
|
+
JKorf/Polymarket.Net; the official client defaults to zero attribution.)
|
|
140
|
+
|
|
141
|
+
## MCP server (agents)
|
|
142
|
+
|
|
143
|
+
`pip install "pmquant[mcp]"` then run `pmq-mcp` (stdio). Read tools (market,
|
|
144
|
+
book, taker_fee, account_collateral, account_trades) always exist. Trading
|
|
145
|
+
tools (`fak_buy`, `fak_sell`, `cancel_and_reconcile`) are **only registered
|
|
146
|
+
when the operator sets `PMQ_MCP_LIVE=1`** in the server environment: an
|
|
147
|
+
agent cannot talk its way past a tool that was never created. Every order is
|
|
148
|
+
capped per call by `PMQ_MCP_MAX_USD` (default 10).
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"mcpServers": {
|
|
153
|
+
"pmq": {
|
|
154
|
+
"command": "pmq-mcp",
|
|
155
|
+
"env": { "POLY_PRIVATE_KEY": "...", "POLY_FUNDER": "0x...", "POLY_SIG_TYPE": "3" }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Leave the `POLY_*` variables out entirely for a read-only market-data server.
|
|
162
|
+
|
|
163
|
+
## Bot template
|
|
164
|
+
|
|
165
|
+
[bot-template/](bot-template/) is a complete bot minus the strategy, for ANY
|
|
166
|
+
market (politics, sports, crypto, culture): paper mode against real books
|
|
167
|
+
with real fees, per-market budgets with fee headroom, poisoned-market
|
|
168
|
+
reconciliation, consecutive-failure halt, disk-persisted daily loss halt, a
|
|
169
|
+
systemd unit with `RestartPreventExitStatus=42` so halts stay halted, and a
|
|
170
|
+
lightweight phone dashboard. You implement `watchlist()` and `decide()`; the
|
|
171
|
+
shipped demo strategy is an API illustration meant to be replaced.
|
|
172
|
+
|
|
173
|
+
## Security posture
|
|
174
|
+
|
|
175
|
+
* Keys are read from the environment, used to instantiate the signer, and
|
|
176
|
+
never logged. No custody, no backend, no telemetry, zero network calls
|
|
177
|
+
besides Polymarket endpoints.
|
|
178
|
+
* Beware of the documented wave of fake "polymarket bot" repositories that
|
|
179
|
+
steal private keys. Read the source: pmq is small on purpose.
|
|
180
|
+
* Fund the trading wallet with what you can afford to lose. Nothing here is
|
|
181
|
+
financial advice; prediction-market access is restricted in some
|
|
182
|
+
jurisdictions and compliance is on you.
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# pmq bot template
|
|
2
|
+
|
|
3
|
+
A complete Polymarket bot minus the strategy, for ANY market: politics,
|
|
4
|
+
sports, crypto, culture. The engine (`bot.py`) is market-agnostic: it tracks
|
|
5
|
+
whatever slugs your `strategy.py` returns, executes your intents through the
|
|
6
|
+
pmq fail-closed layer, scores at resolution, and enforces the risk rails.
|
|
7
|
+
You write two small functions.
|
|
8
|
+
|
|
9
|
+
## What is deliberately NOT here
|
|
10
|
+
|
|
11
|
+
A trading edge, or even a direction to look for one. The shipped demo
|
|
12
|
+
strategy buys 2$ of any 0.98+ favorite once per market: an API illustration
|
|
13
|
+
with negative expectancy, present only so paper mode has something to show.
|
|
14
|
+
The engine is the part that is the same for everyone; the strategy is the
|
|
15
|
+
part that is not.
|
|
16
|
+
|
|
17
|
+
## Quickstart (paper, safe anywhere)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install pmquant
|
|
21
|
+
BOT_SLUGS="<any-gamma-market-slug>" python bot.py 24
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Fills land in `bot_runs/fills.csv`, resolved markets in
|
|
25
|
+
`bot_runs/windows.csv`, and `python dash/bot_dash.py` serves a phone
|
|
26
|
+
dashboard on port 8080 reading those files.
|
|
27
|
+
|
|
28
|
+
## The two functions you own (`strategy.py`)
|
|
29
|
+
|
|
30
|
+
* `watchlist()` returns the gamma slugs to track right now. Static from an
|
|
31
|
+
env var, discovered via the gamma API, or generated for recurring
|
|
32
|
+
markets.
|
|
33
|
+
* `decide(pm, book_a, book_b, remaining_usd, side_held, state)` returns
|
|
34
|
+
`None` or `(side_key, price_cap, usd)`. You see real-time books and the
|
|
35
|
+
market metadata (`pm['outcome_a']`, `pm['end_ts']`, ...); the engine
|
|
36
|
+
enforces budget with fee headroom, one side per market, poisoning,
|
|
37
|
+
reconciliation and halts. A buggy strategy can lose its stake; it cannot
|
|
38
|
+
un-guard the execution.
|
|
39
|
+
|
|
40
|
+
## The rails (engine-enforced, whatever your strategy does)
|
|
41
|
+
|
|
42
|
+
* Paper mode by default; live requires `BOT_MODE=live` AND `LIVE=1`.
|
|
43
|
+
* Nothing booked without exchange confirmation; unknown outcomes poison the
|
|
44
|
+
market until reconciled from `get_trades`.
|
|
45
|
+
* `BOT_MAX_CONSEC_FAILS` failed buys in a row exit with code 42, and the
|
|
46
|
+
systemd unit's `RestartPreventExitStatus=42` keeps that halt halted.
|
|
47
|
+
* Daily realized loss under `BOT_DAILY_HALT` stops entries; in live it is
|
|
48
|
+
persisted to disk so a same-day restart stays stopped.
|
|
49
|
+
* Collateral checked before the first live order (deposit wallets need
|
|
50
|
+
`POLY_SIG_TYPE=3`).
|
|
51
|
+
|
|
52
|
+
## Env reference
|
|
53
|
+
|
|
54
|
+
| Var | Default | Meaning |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| BOT_MODE | paper | paper or live (live also needs LIVE=1) |
|
|
57
|
+
| BOT_SLUGS | empty | comma-separated slugs for the demo watchlist |
|
|
58
|
+
| BOT_STAKE | 5 | $ budget per market, fees included |
|
|
59
|
+
| BOT_DAILY_HALT | -25 | UTC-day realized loss that halts |
|
|
60
|
+
| BOT_POLL | 2.5 | seconds between polls |
|
|
61
|
+
| BOT_SCORE_POLL | 60 | seconds between resolution checks |
|
|
62
|
+
| BOT_FEE_CATEGORY | crypto | fee schedule for budgeting and scoring |
|
|
63
|
+
| BOT_OUT_DIR | ./bot_runs | CSV output directory |
|
|
64
|
+
| BOT_MAX_CONSEC_FAILS | 10 | failed buys in a row before exit 42 |
|
|
65
|
+
| BOT_MAX_TRACK_HOURS | 48 | drop unresolved markets after this long |
|