pmquant 0.4.1__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.
Files changed (40) hide show
  1. {pmquant-0.4.1 → pmquant-0.4.2}/CHANGELOG.md +9 -0
  2. {pmquant-0.4.1 → pmquant-0.4.2}/PKG-INFO +1 -1
  3. {pmquant-0.4.1 → pmquant-0.4.2}/pyproject.toml +1 -1
  4. {pmquant-0.4.1 → pmquant-0.4.2}/server.json +1 -1
  5. {pmquant-0.4.1 → pmquant-0.4.2}/src/pmq/__init__.py +1 -1
  6. {pmquant-0.4.1 → pmquant-0.4.2}/src/pmq/executor.py +14 -2
  7. {pmquant-0.4.1 → pmquant-0.4.2}/tests/test_executor.py +17 -0
  8. {pmquant-0.4.1 → pmquant-0.4.2}/.github/workflows/canary.yml +0 -0
  9. {pmquant-0.4.1 → pmquant-0.4.2}/.github/workflows/publish.yml +0 -0
  10. {pmquant-0.4.1 → pmquant-0.4.2}/.github/workflows/test.yml +0 -0
  11. {pmquant-0.4.1 → pmquant-0.4.2}/.gitignore +0 -0
  12. {pmquant-0.4.1 → pmquant-0.4.2}/AGENTS.md +0 -0
  13. {pmquant-0.4.1 → pmquant-0.4.2}/CLAUDE.md +0 -0
  14. {pmquant-0.4.1 → pmquant-0.4.2}/CONTRIBUTING.md +0 -0
  15. {pmquant-0.4.1 → pmquant-0.4.2}/LICENSE +0 -0
  16. {pmquant-0.4.1 → pmquant-0.4.2}/README.md +0 -0
  17. {pmquant-0.4.1 → pmquant-0.4.2}/SECURITY.md +0 -0
  18. {pmquant-0.4.1 → pmquant-0.4.2}/bot-template/README.md +0 -0
  19. {pmquant-0.4.1 → pmquant-0.4.2}/bot-template/bot.py +0 -0
  20. {pmquant-0.4.1 → pmquant-0.4.2}/bot-template/dash/bot_dash.py +0 -0
  21. {pmquant-0.4.1 → pmquant-0.4.2}/bot-template/dash/dash.html +0 -0
  22. {pmquant-0.4.1 → pmquant-0.4.2}/bot-template/pmq-bot.service +0 -0
  23. {pmquant-0.4.1 → pmquant-0.4.2}/bot-template/strategy.py +0 -0
  24. {pmquant-0.4.1 → pmquant-0.4.2}/docs/assets/pmq-doctor.svg +0 -0
  25. {pmquant-0.4.1 → pmquant-0.4.2}/docs/recipes.md +0 -0
  26. {pmquant-0.4.1 → pmquant-0.4.2}/docs/rounding-study.md +0 -0
  27. {pmquant-0.4.1 → pmquant-0.4.2}/docs/war-story.md +0 -0
  28. {pmquant-0.4.1 → pmquant-0.4.2}/examples/fak_buy_guarded.py +0 -0
  29. {pmquant-0.4.1 → pmquant-0.4.2}/examples/read_market.py +0 -0
  30. {pmquant-0.4.1 → pmquant-0.4.2}/llms.txt +0 -0
  31. {pmquant-0.4.1 → pmquant-0.4.2}/src/pmq/data.py +0 -0
  32. {pmquant-0.4.1 → pmquant-0.4.2}/src/pmq/doctor.py +0 -0
  33. {pmquant-0.4.1 → pmquant-0.4.2}/src/pmq/exceptions.py +0 -0
  34. {pmquant-0.4.1 → pmquant-0.4.2}/src/pmq/mcp.py +0 -0
  35. {pmquant-0.4.1 → pmquant-0.4.2}/src/pmq/py.typed +0 -0
  36. {pmquant-0.4.1 → pmquant-0.4.2}/tests/test_canary_live.py +0 -0
  37. {pmquant-0.4.1 → pmquant-0.4.2}/tests/test_data.py +0 -0
  38. {pmquant-0.4.1 → pmquant-0.4.2}/tests/test_doctor.py +0 -0
  39. {pmquant-0.4.1 → pmquant-0.4.2}/tests/test_mcp.py +0 -0
  40. {pmquant-0.4.1 → pmquant-0.4.2}/tests/test_template_engine.py +0 -0
@@ -1,5 +1,14 @@
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
+
3
12
  ## 0.4.1 (2026-07-03)
4
13
 
5
14
  * Introspection guard now also verifies `OrderArgsV2` fields (including
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pmquant
3
- Version: 0.4.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pmquant"
7
- version = "0.4.1"
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" }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.crp4222/pmq",
4
4
  "title": "pmq (Polymarket CLOB V2)",
5
- "description": "Fail-closed Polymarket CLOB V2 data and execution. Read tools (markets, book, fees, account) need no credentials; trading tools are registered only when the operator sets PMQ_MCP_LIVE=1, with a per-order USD cap.",
5
+ "description": "Fail-closed Polymarket CLOB V2 data and execution; trading tools gated behind PMQ_MCP_LIVE=1.",
6
6
  "repository": {
7
7
  "url": "https://github.com/crp4222/pmq",
8
8
  "source": "github"
@@ -25,7 +25,7 @@ from .data import (
25
25
  )
26
26
  from .exceptions import IntrospectionMismatch, OrderUncertain, PmqError
27
27
 
28
- __version__ = "0.4.1"
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",
@@ -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:
@@ -264,7 +276,7 @@ class PolymarketExecutor:
264
276
  killed by the exchange; nothing ever rests. Returns a :class:`Fill`;
265
277
  book ONLY ``fill.matched_shares`` and ``fill.matched_usd``.
266
278
  Raises :class:`OrderUncertain` when the outcome is unknown."""
267
- usd = int(usd * 100) / 100.0
279
+ usd = _floor_cents(usd)
268
280
  if usd <= 0:
269
281
  return Fill(rejected=True, error="usd amount rounds to zero")
270
282
  return self._market_order(token_id, usd, "BUY", price_cap)
@@ -274,7 +286,7 @@ class PolymarketExecutor:
274
286
  ``price_floor``. Same confirmation contract as :meth:`buy_fak`.
275
287
  The buy path has carried live volume; the sell path follows the same
276
288
  documented semantics but flag it as less battle-tested."""
277
- shares = int(shares * 100) / 100.0
289
+ shares = _floor_cents(shares)
278
290
  if shares <= 0:
279
291
  return Fill(rejected=True, error="share amount rounds to zero")
280
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)
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