pmquant 0.4.0__tar.gz → 0.4.2__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.0 → pmquant-0.4.2}/CHANGELOG.md +19 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/PKG-INFO +3 -1
- {pmquant-0.4.0 → pmquant-0.4.2}/README.md +2 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/pyproject.toml +1 -1
- pmquant-0.4.2/server.json +61 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/src/pmq/__init__.py +1 -1
- {pmquant-0.4.0 → pmquant-0.4.2}/src/pmq/doctor.py +13 -3
- {pmquant-0.4.0 → pmquant-0.4.2}/src/pmq/executor.py +21 -2
- {pmquant-0.4.0 → pmquant-0.4.2}/tests/test_executor.py +30 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/.github/workflows/canary.yml +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/.github/workflows/publish.yml +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/.github/workflows/test.yml +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/.gitignore +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/AGENTS.md +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/CLAUDE.md +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/CONTRIBUTING.md +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/LICENSE +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/SECURITY.md +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/bot-template/README.md +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/bot-template/bot.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/bot-template/dash/bot_dash.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/bot-template/dash/dash.html +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/bot-template/pmq-bot.service +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/bot-template/strategy.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/docs/assets/pmq-doctor.svg +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/docs/recipes.md +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/docs/rounding-study.md +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/docs/war-story.md +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/examples/fak_buy_guarded.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/examples/read_market.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/llms.txt +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/src/pmq/data.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/src/pmq/exceptions.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/src/pmq/mcp.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/src/pmq/py.typed +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/tests/test_canary_live.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/tests/test_data.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/tests/test_doctor.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/tests/test_mcp.py +0 -0
- {pmquant-0.4.0 → pmquant-0.4.2}/tests/test_template_engine.py +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2 (2026-07-03)
|
|
4
|
+
|
|
5
|
+
* Fix: `buy_fak`/`sell_fak` cent-rounding used `int(x * 100) / 100`, which
|
|
6
|
+
floors a binary-drifted float (`16.90` stored as `16.8999…` became
|
|
7
|
+
`16.89`), silently shaving a cent off intended-clean amounts. Replaced with
|
|
8
|
+
a `Decimal`-based `_floor_cents` that rounds down without the drift, keeping
|
|
9
|
+
the never-exceed-budget contract. Matches the behavior documented in
|
|
10
|
+
docs/rounding-study.md.
|
|
11
|
+
|
|
12
|
+
## 0.4.1 (2026-07-03)
|
|
13
|
+
|
|
14
|
+
* Introspection guard now also verifies `OrderArgsV2` fields (including
|
|
15
|
+
`builder_code`, which the limit-order path depends on): a client that
|
|
16
|
+
dropped it is refused at startup instead of failing at call time.
|
|
17
|
+
pmq-doctor mirrors the same check.
|
|
18
|
+
* Distribution: `server.json` manifest for the MCP registry, and an
|
|
19
|
+
`mcp-name` ownership token in the README (visible to the registry's PyPI
|
|
20
|
+
verification, invisible when rendered).
|
|
21
|
+
|
|
3
22
|
## 0.4.0 (2026-07-03)
|
|
4
23
|
|
|
5
24
|
* Typing: the whole public API is annotated and ships a `py.typed` marker;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pmquant
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -34,6 +34,8 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
|
|
35
35
|
# pmq
|
|
36
36
|
|
|
37
|
+
<!-- mcp-name: io.github.crp4222/pmq -->
|
|
38
|
+
|
|
37
39
|
[](https://pypi.org/project/pmquant/)
|
|
38
40
|
[](https://github.com/crp4222/pmq/actions/workflows/test.yml)
|
|
39
41
|
[](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# pmq
|
|
2
2
|
|
|
3
|
+
<!-- mcp-name: io.github.crp4222/pmq -->
|
|
4
|
+
|
|
3
5
|
[](https://pypi.org/project/pmquant/)
|
|
4
6
|
[](https://github.com/crp4222/pmq/actions/workflows/test.yml)
|
|
5
7
|
[](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
|
|
@@ -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.2"
|
|
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" }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.crp4222/pmq",
|
|
4
|
+
"title": "pmq (Polymarket CLOB V2)",
|
|
5
|
+
"description": "Fail-closed Polymarket CLOB V2 data and execution; trading tools gated behind PMQ_MCP_LIVE=1.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/crp4222/pmq",
|
|
8
|
+
"source": "github"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.4.1",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "pypi",
|
|
14
|
+
"registryBaseUrl": "https://pypi.org",
|
|
15
|
+
"identifier": "pmquant",
|
|
16
|
+
"version": "0.4.1",
|
|
17
|
+
"runtimeHint": "uvx",
|
|
18
|
+
"transport": {
|
|
19
|
+
"type": "stdio"
|
|
20
|
+
},
|
|
21
|
+
"environmentVariables": [
|
|
22
|
+
{
|
|
23
|
+
"name": "PMQ_MCP_LIVE",
|
|
24
|
+
"description": "Set to 1 to register the trading tools (fak_buy, fak_sell, cancel_and_reconcile). Unset or any other value keeps the server read-only.",
|
|
25
|
+
"isRequired": false,
|
|
26
|
+
"isSecret": false
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "PMQ_MCP_MAX_USD",
|
|
30
|
+
"description": "Per-order hard cap in USD enforced before any live buy (default 10). Only relevant when PMQ_MCP_LIVE=1.",
|
|
31
|
+
"isRequired": false,
|
|
32
|
+
"isSecret": false
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "POLY_PRIVATE_KEY",
|
|
36
|
+
"description": "EOA private key used only to sign orders locally when trading tools are enabled. Never printed or transmitted. Not needed for the read-only tools.",
|
|
37
|
+
"isRequired": false,
|
|
38
|
+
"isSecret": true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "POLY_FUNDER",
|
|
42
|
+
"description": "Funder wallet address. Required with signature_type > 0 (deposit wallets). Only used when trading tools are enabled.",
|
|
43
|
+
"isRequired": false,
|
|
44
|
+
"isSecret": false
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "POLY_SIG_TYPE",
|
|
48
|
+
"description": "Polymarket signature type: 0 EOA, 1 POLY_PROXY, 2 POLY_GNOSIS_SAFE, 3 POLY_1271 (app deposit wallet). Run pmq-doctor if unsure.",
|
|
49
|
+
"isRequired": false,
|
|
50
|
+
"isSecret": false
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "POLY_BUILDER_CODE",
|
|
54
|
+
"description": "Optional bytes32 builder attribution code. Defaults to the maintainer's public zero-commission code (disclosed in the README); set your own or leave empty to opt out.",
|
|
55
|
+
"isRequired": false,
|
|
56
|
+
"isSecret": false
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -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.2"
|
|
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",
|
|
@@ -81,9 +81,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
81
81
|
# 1. installed surface
|
|
82
82
|
try:
|
|
83
83
|
from py_clob_client_v2.client import ClobClient
|
|
84
|
-
from py_clob_client_v2.clob_types import
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
from py_clob_client_v2.clob_types import (
|
|
85
|
+
MarketOrderArgsV2,
|
|
86
|
+
OrderArgsV2,
|
|
87
|
+
OrderType,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
from .executor import (
|
|
91
|
+
_EXPECTED_MARKET_ARGS,
|
|
92
|
+
_EXPECTED_METHODS,
|
|
93
|
+
_EXPECTED_ORDER_ARGS,
|
|
94
|
+
)
|
|
87
95
|
drifts = []
|
|
88
96
|
for name, params in _EXPECTED_METHODS.items():
|
|
89
97
|
fn = getattr(ClobClient, name, None)
|
|
@@ -91,6 +99,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
91
99
|
drifts += [f"{name}.{p}" for p in params if p not in have]
|
|
92
100
|
have = set(inspect.signature(MarketOrderArgsV2).parameters)
|
|
93
101
|
drifts += [f"MarketOrderArgsV2.{p}" for p in _EXPECTED_MARKET_ARGS if p not in have]
|
|
102
|
+
have = set(inspect.signature(OrderArgsV2).parameters)
|
|
103
|
+
drifts += [f"OrderArgsV2.{p}" for p in _EXPECTED_ORDER_ARGS if p not in have]
|
|
94
104
|
if not hasattr(OrderType, "FAK"):
|
|
95
105
|
drifts.append("OrderType.FAK")
|
|
96
106
|
all_ok &= check(not drifts, "installed py-clob-client-v2 matches the verified surface",
|
|
@@ -36,6 +36,7 @@ import logging
|
|
|
36
36
|
import os
|
|
37
37
|
import re
|
|
38
38
|
from dataclasses import dataclass, field
|
|
39
|
+
from decimal import ROUND_DOWN, Decimal
|
|
39
40
|
from typing import Any
|
|
40
41
|
|
|
41
42
|
from .data import FEE_RATES, fee
|
|
@@ -55,6 +56,17 @@ BYTES32_RE = re.compile(r"0x[0-9a-fA-F]{64}")
|
|
|
55
56
|
DEFAULT_BUILDER_CODE = "0x4b22812cf929165a247b575eb417a3b6c9e3c12e96f0159c4d0ad39f78d17371"
|
|
56
57
|
_UNSET: Any = object()
|
|
57
58
|
|
|
59
|
+
_CENT = Decimal("0.01")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _floor_cents(x: float) -> float:
|
|
63
|
+
"""Round DOWN to the cent without the binary-float drift that makes
|
|
64
|
+
``int(16.90 * 100) / 100`` return ``16.89``. ``str(x)`` yields the shortest
|
|
65
|
+
decimal repr, so ``16.90`` stays ``16.90`` while a genuine ``16.907`` still
|
|
66
|
+
floors to ``16.90``. Rounding down preserves the never-exceed-budget
|
|
67
|
+
contract for buys and the never-oversell contract for sells."""
|
|
68
|
+
return float(Decimal(str(x)).quantize(_CENT, rounding=ROUND_DOWN))
|
|
69
|
+
|
|
58
70
|
|
|
59
71
|
@dataclass
|
|
60
72
|
class Fill:
|
|
@@ -87,6 +99,8 @@ _EXPECTED_METHODS: dict[str, tuple[str, ...]] = {
|
|
|
87
99
|
}
|
|
88
100
|
_EXPECTED_MARKET_ARGS: tuple[str, ...] = (
|
|
89
101
|
"token_id", "amount", "side", "price", "builder_code")
|
|
102
|
+
_EXPECTED_ORDER_ARGS: tuple[str, ...] = (
|
|
103
|
+
"token_id", "price", "size", "side", "builder_code")
|
|
90
104
|
|
|
91
105
|
|
|
92
106
|
class PolymarketExecutor:
|
|
@@ -176,6 +190,11 @@ class PolymarketExecutor:
|
|
|
176
190
|
for p in _EXPECTED_MARKET_ARGS:
|
|
177
191
|
if p not in have:
|
|
178
192
|
drifts.append(f"MarketOrderArgsV2 lost field {p}")
|
|
193
|
+
oargs = self._t["OrderArgs"]
|
|
194
|
+
have = set(inspect.signature(oargs).parameters)
|
|
195
|
+
for p in _EXPECTED_ORDER_ARGS:
|
|
196
|
+
if p not in have:
|
|
197
|
+
drifts.append(f"OrderArgsV2 lost field {p}")
|
|
179
198
|
if not hasattr(self._t["OrderType"], "FAK"):
|
|
180
199
|
drifts.append("OrderType.FAK missing")
|
|
181
200
|
if drifts:
|
|
@@ -257,7 +276,7 @@ class PolymarketExecutor:
|
|
|
257
276
|
killed by the exchange; nothing ever rests. Returns a :class:`Fill`;
|
|
258
277
|
book ONLY ``fill.matched_shares`` and ``fill.matched_usd``.
|
|
259
278
|
Raises :class:`OrderUncertain` when the outcome is unknown."""
|
|
260
|
-
usd =
|
|
279
|
+
usd = _floor_cents(usd)
|
|
261
280
|
if usd <= 0:
|
|
262
281
|
return Fill(rejected=True, error="usd amount rounds to zero")
|
|
263
282
|
return self._market_order(token_id, usd, "BUY", price_cap)
|
|
@@ -267,7 +286,7 @@ class PolymarketExecutor:
|
|
|
267
286
|
``price_floor``. Same confirmation contract as :meth:`buy_fak`.
|
|
268
287
|
The buy path has carried live volume; the sell path follows the same
|
|
269
288
|
documented semantics but flag it as less battle-tested."""
|
|
270
|
-
shares =
|
|
289
|
+
shares = _floor_cents(shares)
|
|
271
290
|
if shares <= 0:
|
|
272
291
|
return Fill(rejected=True, error="share amount rounds to zero")
|
|
273
292
|
return self._market_order(token_id, shares, "SELL", price_floor)
|
|
@@ -114,6 +114,23 @@ def test_buy_amount_rounds_down_to_cent():
|
|
|
114
114
|
assert fc.calls[0][1].amount == 4.99
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
def test_cent_rounding_has_no_binary_drift():
|
|
118
|
+
from pmq.executor import _floor_cents
|
|
119
|
+
# int(x*100)/100 would return one cent low here; _floor_cents must not.
|
|
120
|
+
assert _floor_cents(16.90) == 16.90
|
|
121
|
+
assert _floor_cents(33.30) == 33.30
|
|
122
|
+
assert _floor_cents(66.60) == 66.60
|
|
123
|
+
# genuine sub-cent values still floor
|
|
124
|
+
assert _floor_cents(5.007) == 5.00
|
|
125
|
+
assert _floor_cents(4.999) == 4.99
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_buy_fak_sends_intended_cents_not_drifted():
|
|
129
|
+
fc = FakeClient(market_resp={"orderID": "0x1", "makingAmount": "0", "takingAmount": "0"})
|
|
130
|
+
make(fc).buy_fak("tok", 0.97, 16.90)
|
|
131
|
+
assert fc.calls[0][1].amount == 16.90 # not 16.89
|
|
132
|
+
|
|
133
|
+
|
|
117
134
|
def test_zero_amount_never_reaches_the_wire():
|
|
118
135
|
fc = FakeClient()
|
|
119
136
|
f = make(fc).buy_fak("tok", 0.97, 0.004)
|
|
@@ -255,6 +272,19 @@ def test_introspection_guard_refuses_drifted_client():
|
|
|
255
272
|
make(Drifted())
|
|
256
273
|
|
|
257
274
|
|
|
275
|
+
def test_introspection_guard_covers_order_args_builder_code(monkeypatch):
|
|
276
|
+
# limit_gtc now depends on OrderArgsV2.builder_code; the guard must refuse
|
|
277
|
+
# a client whose OrderArgsV2 dropped it, not crash later at call time.
|
|
278
|
+
import py_clob_client_v2.clob_types as ct
|
|
279
|
+
|
|
280
|
+
class NoBuilderOrderArgs:
|
|
281
|
+
def __init__(self, token_id, price, size, side):
|
|
282
|
+
self.token_id, self.price, self.size, self.side = token_id, price, size, side
|
|
283
|
+
monkeypatch.setattr(ct, "OrderArgsV2", NoBuilderOrderArgs)
|
|
284
|
+
with pytest.raises(IntrospectionMismatch, match="OrderArgsV2 lost field builder_code"):
|
|
285
|
+
make(FakeClient())
|
|
286
|
+
|
|
287
|
+
|
|
258
288
|
def test_reconcile_cancels_then_reports_truth():
|
|
259
289
|
fc = FakeClient(trades=[{"side": "BUY", "size": "2", "price": "0.95"}],
|
|
260
290
|
open_orders=[])
|
|
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
|