pmquant 0.1.0__py3-none-any.whl

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.
pmq/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """pmq: fail-closed execution and market data for Polymarket CLOB V2.
2
+
3
+ Data layer (no keys needed): get_market, parse_market, get_book,
4
+ best_bid_ask, band_ask_depth_usd, resolved_winner, book_inferred_winner,
5
+ get_tape, fee, FEE_RATES.
6
+
7
+ Execution layer (keys stay local, nothing booked without exchange
8
+ confirmation): PolymarketExecutor, Fill, OrderUncertain.
9
+ """
10
+ from .data import (FEE_RATES, band_ask_depth_usd, best_bid_ask,
11
+ book_inferred_winner, book_meta, fee, get_book, get_market,
12
+ get_tape, http_get_json, parse_market, resolved_winner)
13
+ from .exceptions import IntrospectionMismatch, OrderUncertain, PmqError
14
+
15
+ __version__ = "0.1.0"
16
+ __all__ = [
17
+ "FEE_RATES", "band_ask_depth_usd", "best_bid_ask", "book_inferred_winner",
18
+ "book_meta", "fee", "get_book", "get_market", "get_tape", "http_get_json",
19
+ "parse_market", "resolved_winner",
20
+ "PolymarketExecutor", "Fill",
21
+ "PmqError", "OrderUncertain", "IntrospectionMismatch",
22
+ "__version__",
23
+ ]
24
+
25
+
26
+ def __getattr__(name):
27
+ # Lazy import: the data layer must stay usable without py-clob-client-v2.
28
+ if name in ("PolymarketExecutor", "Fill", "DEFAULT_BUILDER_CODE"):
29
+ from . import executor
30
+ return getattr(executor, name)
31
+ raise AttributeError(name)
pmq/data.py ADDED
@@ -0,0 +1,208 @@
1
+ """Market data layer for Polymarket, with the operational knowledge encoded.
2
+
3
+ Facts this module encodes (verified live, July 2026):
4
+
5
+ * The CLOB ``/book`` endpoint is REAL TIME (served by the matching engine,
6
+ includes ``last_trade_price``). The data-api ``/trades`` indexer LAGS
7
+ matching by 1 to 3 minutes (measured: freshest visible trade 120s old).
8
+ Therefore: books drive live decisions; the trade tape is for OFFLINE
9
+ scoring only, at least 5 minutes after the window closes.
10
+ * Gamma ``/markets?slug=`` returns ``[]`` for EXPIRED short-lived markets;
11
+ ``/events?slug=`` still resolves them. :func:`get_market` does the
12
+ fallback for you.
13
+ * Gamma settlement (outcomePrices pinned to 0.99+) can lag the market close
14
+ by more than 15 minutes; the last pre-close book identifies the winner
15
+ immediately (a side bid pinned at 0.90+ means that side won). See
16
+ :func:`book_inferred_winner`.
17
+ * Taker fees (since 2026-03-30, decided at match time under CLOB V2):
18
+ ``fee = rate * price * (1 - price) * shares`` with a per-category rate,
19
+ see :data:`FEE_RATES`. Makers always pay zero. The ``maker/taker_base_fee``
20
+ of 1000 bps seen in API responses is an on-chain CAP, never the charge.
21
+ """
22
+ import calendar
23
+ import json
24
+ import time
25
+ import urllib.request
26
+
27
+ UA = {"User-Agent": "Mozilla/5.0"}
28
+ GAMMA = "https://gamma-api.polymarket.com"
29
+ CLOB = "https://clob.polymarket.com"
30
+ DATA = "https://data-api.polymarket.com"
31
+
32
+ #: Official taker fee rates per market category (docs.polymarket.com/trading/fees,
33
+ #: fetched 2026-07-03). Fee in $ = rate * p * (1 - p) * shares. Makers pay 0.
34
+ FEE_RATES = {
35
+ "crypto": 0.07,
36
+ "sports": 0.03,
37
+ "finance": 0.04,
38
+ "politics": 0.04,
39
+ "mentions": 0.04,
40
+ "tech": 0.04,
41
+ "economics": 0.05,
42
+ "culture": 0.05,
43
+ "weather": 0.05,
44
+ "geopolitics": 0.0,
45
+ }
46
+
47
+
48
+ def fee(price, shares, rate=FEE_RATES["crypto"]):
49
+ """Taker fee in $ under the current schedule. Makers pay zero.
50
+
51
+ The fee peaks at price 0.50 and vanishes toward 0 and 1: a taker fill at
52
+ 0.95 costs about a third of one at 0.50 for the same share count.
53
+ """
54
+ return rate * price * (1.0 - price) * shares
55
+
56
+
57
+ def http_get_json(url, retries=3, timeout=10, logger=None):
58
+ """GET a JSON document with linear backoff. Returns None on final failure."""
59
+ for i in range(retries):
60
+ try:
61
+ req = urllib.request.Request(url, headers=UA)
62
+ with urllib.request.urlopen(req, timeout=timeout) as r:
63
+ return json.loads(r.read().decode())
64
+ except Exception as e:
65
+ if i == retries - 1:
66
+ if logger:
67
+ logger(f"GET failed permanently: {url} ({e})")
68
+ return None
69
+ time.sleep(1.5 * (i + 1))
70
+ return None
71
+
72
+
73
+ def get_market(slug, logger=None):
74
+ """Gamma market object for a slug, falling back to /events for expired ones."""
75
+ data = http_get_json(f"{GAMMA}/markets?slug={slug}", logger=logger)
76
+ if data:
77
+ return data[0]
78
+ ev = http_get_json(f"{GAMMA}/events?slug={slug}", logger=logger)
79
+ if ev and ev[0].get("markets"):
80
+ return ev[0]["markets"][0]
81
+ return None
82
+
83
+
84
+ def _end_ts(m):
85
+ """Market close time as unix epoch, or None. Gamma sends UTC ISO 8601."""
86
+ raw = m.get("endDate") or m.get("endDateIso") or ""
87
+ for fmt in ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S.%fZ"):
88
+ try:
89
+ return calendar.timegm(time.strptime(raw, fmt))
90
+ except (ValueError, OverflowError):
91
+ continue
92
+ return None
93
+
94
+
95
+ def parse_market(m, outcome_a=None, outcome_b=None):
96
+ """Extract condition id and outcome token ids from a Gamma market object.
97
+
98
+ Works on ANY binary market: politics, sports, crypto, whatever the
99
+ outcome names are (Yes/No, Up/Down, team names). By default the two
100
+ outcomes are taken in the order the market declares them; pass
101
+ ``outcome_a``/``outcome_b`` to pin a specific one to the ``a`` slot.
102
+ Returns None on any shape surprise (fail closed).
103
+ """
104
+ if not m:
105
+ return None
106
+ try:
107
+ outcomes = json.loads(m["outcomes"]) if isinstance(m.get("outcomes"), str) else m.get("outcomes")
108
+ token_ids = json.loads(m["clobTokenIds"]) if isinstance(m.get("clobTokenIds"), str) else m.get("clobTokenIds")
109
+ a = outcomes.index(outcome_a) if outcome_a else 0
110
+ b = outcomes.index(outcome_b) if outcome_b else (1 if a == 0 else 0)
111
+ return {
112
+ "condition_id": m.get("conditionId"),
113
+ "slug": m.get("slug"),
114
+ "token_a": token_ids[a],
115
+ "token_b": token_ids[b],
116
+ "outcome_a": outcomes[a],
117
+ "outcome_b": outcomes[b],
118
+ "outcome_prices_raw": m.get("outcomePrices"),
119
+ "idx_a": a,
120
+ "end_ts": _end_ts(m),
121
+ }
122
+ except Exception:
123
+ return None
124
+
125
+
126
+ def resolved_winner(pm):
127
+ """Winning outcome name from settled Gamma outcomePrices; None if unsettled."""
128
+ if not pm:
129
+ return None
130
+ try:
131
+ op = pm["outcome_prices_raw"]
132
+ op = json.loads(op) if isinstance(op, str) else op
133
+ op = [float(x) for x in op]
134
+ if max(op) < 0.99:
135
+ return None
136
+ ia = pm.get("idx_a", 0)
137
+ return pm["outcome_a"] if op[ia] > op[1 - ia] else pm["outcome_b"]
138
+ except Exception:
139
+ return None
140
+
141
+
142
+ def get_book(token_id, logger=None):
143
+ """Real-time CLOB book. THE live data source; never the trade tape."""
144
+ return http_get_json(f"{CLOB}/book?token_id={token_id}", logger=logger)
145
+
146
+
147
+ def best_bid_ask(book):
148
+ """(best_bid, bid_size, best_ask, ask_size), sizes summed at the level."""
149
+ if not book:
150
+ return None, None, None, None
151
+ bids = book.get("bids") or []
152
+ asks = book.get("asks") or []
153
+ bb = max((float(b["price"]) for b in bids), default=None)
154
+ ba = min((float(a["price"]) for a in asks), default=None)
155
+ bb_sz = sum(float(b["size"]) for b in bids if float(b["price"]) == bb) if bb is not None else None
156
+ ba_sz = sum(float(a["size"]) for a in asks if float(a["price"]) == ba) if ba is not None else None
157
+ return bb, bb_sz, ba, ba_sz
158
+
159
+
160
+ def book_meta(book):
161
+ """Exchange metadata riding on the book response: per-market minimum
162
+ order size (shares), tick size, neg_risk flag, last trade price. Read
163
+ these from the live book instead of hardcoding exchange rules."""
164
+ b = book or {}
165
+ def _f(k):
166
+ try:
167
+ return float(b[k])
168
+ except (KeyError, TypeError, ValueError):
169
+ return None
170
+ return {"min_order_size": _f("min_order_size"), "tick_size": _f("tick_size"),
171
+ "neg_risk": b.get("neg_risk"), "last_trade_price": _f("last_trade_price")}
172
+
173
+
174
+ def band_ask_depth_usd(book, lo, hi):
175
+ """Total $ notional of asks resting within [lo, hi]."""
176
+ asks = (book or {}).get("asks") or []
177
+ return round(sum(float(a["price"]) * float(a["size"]) for a in asks
178
+ if lo <= float(a["price"]) <= hi), 2)
179
+
180
+
181
+ def book_inferred_winner(bid_a, bid_b, threshold=0.90):
182
+ """Winner from a last pre-close book snapshot; None if ambiguous.
183
+
184
+ Use when Gamma settlement lags: a side whose BID is pinned at or above
185
+ ``threshold`` at close identifies the winner immediately.
186
+ """
187
+ if bid_a is not None and bid_a >= threshold:
188
+ return "a"
189
+ if bid_b is not None and bid_b >= threshold:
190
+ return "b"
191
+ return None
192
+
193
+
194
+ def get_tape(condition_id, since_ts, max_pages=4, logger=None):
195
+ """Complete trade tape for a closed market (paginated, newest first).
196
+
197
+ OFFLINE USE ONLY: the indexer lags matching by 1 to 3 minutes; call at
198
+ least 5 minutes after close or you will score against missing fills.
199
+ """
200
+ out = []
201
+ for page in range(max_pages):
202
+ batch = http_get_json(
203
+ f"{DATA}/trades?market={condition_id}&limit=500&offset={page*500}",
204
+ logger=logger) or []
205
+ out.extend(batch)
206
+ if not batch or min(int(t.get("timestamp", 0)) for t in batch) < since_ts:
207
+ break
208
+ return out
pmq/exceptions.py ADDED
@@ -0,0 +1,19 @@
1
+ """pmq exceptions. The taxonomy matters: callers must be able to tell a clean
2
+ rejection (order does not exist) from an unknown outcome (order MAY exist)."""
3
+
4
+
5
+ class PmqError(Exception):
6
+ """Base class for pmq errors."""
7
+
8
+
9
+ class OrderUncertain(PmqError):
10
+ """An order was sent but its outcome is unknown (timeout, 5xx, exception
11
+ after send). It MAY exist server-side. The caller must cancel, reconcile
12
+ against exchange truth (get_trades) and stop trading that market until
13
+ reconciled. Never book anything on this exception."""
14
+
15
+
16
+ class IntrospectionMismatch(PmqError):
17
+ """The installed py-clob-client-v2 no longer matches the API surface pmq
18
+ was verified against. Refusing to trade is the only safe behavior: a
19
+ silently changed parameter meaning can turn guards into no-ops."""
pmq/executor.py ADDED
@@ -0,0 +1,329 @@
1
+ """Fail-closed execution layer for Polymarket CLOB V2.
2
+
3
+ Design contract, in one paragraph: nothing is ever booked unless the exchange
4
+ confirmed it. A fill exists only if the order response is a dict bearing an
5
+ orderID and not flagged failed, and only for the matched size the response
6
+ reports. A 4xx is a clean rejection (the order does not exist). A timeout or
7
+ 5xx raises :class:`~pmq.exceptions.OrderUncertain` and the caller must
8
+ reconcile via :meth:`PolymarketExecutor.reconcile` before trading that market
9
+ again. Unparseable responses count as zero fill. This is the difference
10
+ between a bot that survives a flaky network and one that buys its stake again
11
+ on every poll.
12
+
13
+ Production notes baked in (all verified live on CLOB V2, July 2026):
14
+
15
+ * FAK/FOK BUY orders are treated by the CLOB as MARKET orders: the maker
16
+ amount is USDC with at most 2 decimals. Posting them as plain limit orders
17
+ fails with ``invalid amounts, the market buy orders maker amount supports a
18
+ max accuracy of 2 decimals``. pmq routes them through the market-order
19
+ builder, which applies the correct rounding.
20
+ * A 400 ``no orders found to match with FAK order`` WITH an orderID in the
21
+ body is a clean no-fill (the ask vanished between your poll and your send),
22
+ not an error state.
23
+ * The balance endpoint derives the funding wallet server-side from the
24
+ authenticated EOA and ``signature_type``; the ``funder`` parameter is never
25
+ sent. Funds held by a deposit wallet (ERC-1271) are only visible with
26
+ ``signature_type=3`` (POLY_1271).
27
+ * Builder attribution rides INSIDE the signed order (bytes32 code); HTTP
28
+ headers attribute nothing on V2.
29
+ """
30
+ import inspect
31
+ import logging
32
+ import os
33
+ import re
34
+ from dataclasses import dataclass, field
35
+
36
+ from .data import FEE_RATES, fee
37
+ from .exceptions import IntrospectionMismatch, OrderUncertain
38
+
39
+ log = logging.getLogger("pmq")
40
+
41
+ HOST = "https://clob.polymarket.com"
42
+ CHAIN_ID = 137
43
+ BYTES32_RE = re.compile(r"0x[0-9a-fA-F]{64}")
44
+
45
+ # Builder attribution default. DISCLOSURE: this is the pmq maintainer's public
46
+ # builder code (commission 0/0: it never adds fees to your orders). It funds
47
+ # the project through Polymarket's builder program at zero cost to you.
48
+ # Opt out with PolymarketExecutor(builder_code=None) or use your own code
49
+ # from https://polymarket.com/settings?tab=builder
50
+ DEFAULT_BUILDER_CODE = "0x4b22812cf929165a247b575eb417a3b6c9e3c12e96f0159c4d0ad39f78d17371"
51
+ _UNSET = object()
52
+
53
+
54
+ @dataclass
55
+ class Fill:
56
+ """Exchange-confirmed execution result. ``matched_shares == 0`` means no
57
+ fill happened and NOTHING must be booked, whatever the caller hoped."""
58
+ order_id: str = ""
59
+ matched_shares: float = 0.0
60
+ matched_usd: float = 0.0
61
+ rejected: bool = False
62
+ error: str = ""
63
+ raw: dict = field(default_factory=dict)
64
+
65
+ @property
66
+ def price(self):
67
+ return self.matched_usd / self.matched_shares if self.matched_shares else None
68
+
69
+ def __bool__(self):
70
+ return self.matched_shares > 0
71
+
72
+
73
+ # (method name, parameters that must exist) verified against py-clob-client-v2
74
+ # 1.0.2 by introspection. If the installed client drifts, we refuse to start.
75
+ _EXPECTED_METHODS = {
76
+ "create_and_post_market_order": ("order_args", "order_type"),
77
+ "create_and_post_order": ("order_args", "order_type"),
78
+ "cancel_market_orders": ("payload",),
79
+ "get_open_orders": ("params",),
80
+ "get_trades": ("params",),
81
+ "get_balance_allowance": ("params",),
82
+ }
83
+ _EXPECTED_MARKET_ARGS = ("token_id", "amount", "side", "price", "builder_code")
84
+
85
+
86
+ class PolymarketExecutor:
87
+ """Thin, fail-closed wrapper around the official py-clob-client-v2.
88
+
89
+ Reads POLY_PRIVATE_KEY, POLY_FUNDER, POLY_SIG_TYPE and POLY_BUILDER_CODE
90
+ from the environment unless passed explicitly. The private key is used to
91
+ instantiate the signer and is never logged.
92
+
93
+ ``signature_type``: 0 EOA, 1 POLY_PROXY (email/Magic), 2 POLY_GNOSIS_SAFE,
94
+ 3 POLY_1271 (deposit wallet, the default wallet of the Polymarket app).
95
+ If the CLOB shows a 0 balance while the funds sit on your funder address
96
+ on-chain, your signature_type is wrong: only the matching type makes the
97
+ server derive the wallet that actually holds the pUSD.
98
+ """
99
+
100
+ def __init__(self, key=None, funder=None, signature_type=None,
101
+ builder_code=_UNSET, host=HOST, chain_id=CHAIN_ID,
102
+ client=None, derive_creds=True):
103
+ from py_clob_client_v2.clob_types import (
104
+ AssetType, BalanceAllowanceParams, BuilderConfig,
105
+ MarketOrderArgsV2, OpenOrderParams, OrderArgsV2,
106
+ OrderMarketCancelParams, OrderType, TradeParams)
107
+ from py_clob_client_v2.exceptions import PolyApiException
108
+ self._t = {
109
+ "AssetType": AssetType, "BalanceAllowanceParams": BalanceAllowanceParams,
110
+ "MarketOrderArgs": MarketOrderArgsV2, "OpenOrderParams": OpenOrderParams,
111
+ "OrderArgs": OrderArgsV2, "OrderMarketCancelParams": OrderMarketCancelParams,
112
+ "OrderType": OrderType, "TradeParams": TradeParams,
113
+ }
114
+ self.PolyApiException = PolyApiException
115
+
116
+ if builder_code is _UNSET:
117
+ builder_code = os.environ.get("POLY_BUILDER_CODE", DEFAULT_BUILDER_CODE)
118
+ if builder_code and not BYTES32_RE.fullmatch(builder_code):
119
+ raise ValueError("builder_code must be bytes32 hex (0x + 64 hex chars) or None")
120
+ self.builder_code = builder_code
121
+
122
+ if client is not None:
123
+ self.client = client
124
+ else:
125
+ from py_clob_client_v2.client import ClobClient
126
+ key = key or os.environ.get("POLY_PRIVATE_KEY")
127
+ if not key:
128
+ raise ValueError("POLY_PRIVATE_KEY missing (env or key= argument)")
129
+ funder = funder or os.environ.get("POLY_FUNDER")
130
+ if signature_type is None:
131
+ signature_type = int(os.environ.get("POLY_SIG_TYPE", "0"))
132
+ if signature_type > 0 and not funder:
133
+ raise ValueError("signature_type > 0 requires the funder wallet address")
134
+ builder_config = BuilderConfig(builder_code=builder_code) if builder_code else None
135
+ self.client = ClobClient(host, chain_id=chain_id, key=key,
136
+ signature_type=signature_type, funder=funder,
137
+ builder_config=builder_config,
138
+ use_server_time=True, retry_on_error=True)
139
+ if derive_creds:
140
+ self.client.set_api_creds(self.client.create_or_derive_api_key())
141
+ self._verify_client_surface()
142
+ log.info("executor ready (builder=%s)", "on" if builder_code else "off")
143
+
144
+ # ---------------- introspection guard ----------------
145
+ def _verify_client_surface(self):
146
+ drifts = []
147
+ for name, params in _EXPECTED_METHODS.items():
148
+ fn = getattr(self.client, name, None)
149
+ if fn is None:
150
+ drifts.append(f"method {name} missing")
151
+ continue
152
+ try:
153
+ have = set(inspect.signature(fn).parameters)
154
+ except (TypeError, ValueError):
155
+ continue
156
+ for p in params:
157
+ if p not in have:
158
+ drifts.append(f"{name}() lost parameter {p}")
159
+ margs = self._t["MarketOrderArgs"]
160
+ have = set(inspect.signature(margs).parameters)
161
+ for p in _EXPECTED_MARKET_ARGS:
162
+ if p not in have:
163
+ drifts.append(f"MarketOrderArgsV2 lost field {p}")
164
+ if not hasattr(self._t["OrderType"], "FAK"):
165
+ drifts.append("OrderType.FAK missing")
166
+ if drifts:
167
+ raise IntrospectionMismatch(
168
+ "installed py-clob-client-v2 drifted from the verified surface: "
169
+ + "; ".join(drifts) + ". Refusing to trade; pin the version pmq "
170
+ "was tested with or upgrade pmq.")
171
+
172
+ # ---------------- balance ----------------
173
+ def collateral(self):
174
+ """Available pUSD collateral as seen by the CLOB for this EOA and
175
+ signature_type. Raw 6-decimal units converted to $. Any parse failure
176
+ returns 0.0 (fail closed)."""
177
+ try:
178
+ bal = self.client.get_balance_allowance(self._t["BalanceAllowanceParams"](
179
+ asset_type=self._t["AssetType"].COLLATERAL))
180
+ return float(bal.get("balance", 0)) / 1e6 if isinstance(bal, dict) else 0.0
181
+ except Exception as e:
182
+ log.warning("collateral check failed: %s", e)
183
+ return 0.0
184
+
185
+ def require_collateral(self, min_usd):
186
+ """Raise if the CLOB-visible collateral is below ``min_usd``."""
187
+ usdc = self.collateral()
188
+ if usdc < min_usd:
189
+ raise RuntimeError(
190
+ f"collateral {usdc:.2f} USDC below required {min_usd:.2f}. If the "
191
+ f"funds ARE on your funder address on-chain, your signature_type "
192
+ f"is wrong (deposit wallets need 3).")
193
+ return usdc
194
+
195
+ # ---------------- orders ----------------
196
+ def _parse_fill(self, resp, side):
197
+ if not (isinstance(resp, dict) and resp.get("orderID")
198
+ and resp.get("success") is not False):
199
+ return Fill(rejected=True, error=repr(resp)[:300],
200
+ raw=resp if isinstance(resp, dict) else {})
201
+ # For a BUY the maker amount is USDC given and the taker amount shares
202
+ # received; a SELL mirrors it. Unparseable counts as zero (fail closed).
203
+ try:
204
+ making = float(resp.get("makingAmount") or 0.0)
205
+ taking = float(resp.get("takingAmount") or 0.0)
206
+ except (TypeError, ValueError):
207
+ making = taking = 0.0
208
+ usd, shares = (making, taking) if side == "BUY" else (taking, making)
209
+ return Fill(order_id=str(resp["orderID"]), matched_shares=shares,
210
+ matched_usd=usd, raw=resp)
211
+
212
+ def _market_order(self, token_id, amount, side, price):
213
+ t = self._t
214
+ try:
215
+ resp = self.client.create_and_post_market_order(
216
+ t["MarketOrderArgs"](token_id=token_id, amount=amount,
217
+ side=side, price=price),
218
+ order_type=t["OrderType"].FAK)
219
+ except self.PolyApiException as e:
220
+ status = getattr(e, "status_code", None)
221
+ msg = str(getattr(e, "error_msg", e))[:300]
222
+ if status is not None and 400 <= status < 500:
223
+ # clean rejection, the order does not exist server-side;
224
+ # "no orders found to match" lands here and is a no-fill
225
+ return Fill(rejected=True, error=f"{status}: {msg}")
226
+ raise OrderUncertain(f"status={status} {msg}")
227
+ except Exception as e:
228
+ raise OrderUncertain(repr(e)[:300])
229
+ return self._parse_fill(resp, side)
230
+
231
+ def buy_fak(self, token_id, price_cap, usd):
232
+ """Fill-and-kill market BUY: spend up to ``usd`` (rounded DOWN to the
233
+ cent, the exchange accuracy for market-buy maker amounts) at prices no
234
+ worse than ``price_cap``. Whatever does not match immediately is
235
+ killed by the exchange; nothing ever rests. Returns a :class:`Fill`;
236
+ book ONLY ``fill.matched_shares`` and ``fill.matched_usd``.
237
+ Raises :class:`OrderUncertain` when the outcome is unknown."""
238
+ usd = int(usd * 100) / 100.0
239
+ if usd <= 0:
240
+ return Fill(rejected=True, error="usd amount rounds to zero")
241
+ return self._market_order(token_id, usd, "BUY", price_cap)
242
+
243
+ def sell_fak(self, token_id, price_floor, shares):
244
+ """Fill-and-kill market SELL of ``shares`` at prices no worse than
245
+ ``price_floor``. Same confirmation contract as :meth:`buy_fak`.
246
+ The buy path has carried live volume; the sell path follows the same
247
+ documented semantics but flag it as less battle-tested."""
248
+ shares = int(shares * 100) / 100.0
249
+ if shares <= 0:
250
+ return Fill(rejected=True, error="share amount rounds to zero")
251
+ return self._market_order(token_id, shares, "SELL", price_floor)
252
+
253
+ def limit_gtc(self, token_id, price, size, side, post_only=False):
254
+ """Resting GTC limit order. Returns a :class:`Fill` whose matched
255
+ amounts reflect only what crossed IMMEDIATELY; the rest is resting
256
+ (track it via :meth:`open_orders`, settle via :meth:`trades_totals`).
257
+ Maker fills cost zero fee."""
258
+ t = self._t
259
+ try:
260
+ resp = self.client.create_and_post_order(
261
+ t["OrderArgs"](token_id=token_id, price=price, size=size, side=side),
262
+ order_type=t["OrderType"].GTC, post_only=post_only)
263
+ except self.PolyApiException as e:
264
+ status = getattr(e, "status_code", None)
265
+ msg = str(getattr(e, "error_msg", e))[:300]
266
+ if status is not None and 400 <= status < 500:
267
+ return Fill(rejected=True, error=f"{status}: {msg}")
268
+ raise OrderUncertain(f"status={status} {msg}")
269
+ except Exception as e:
270
+ raise OrderUncertain(repr(e)[:300])
271
+ return self._parse_fill(resp, side)
272
+
273
+ # ---------------- reconciliation ----------------
274
+ def cancel_market(self, condition_id):
275
+ """Cancel every resting order of ours on one market. Never raises."""
276
+ try:
277
+ self.client.cancel_market_orders(
278
+ self._t["OrderMarketCancelParams"](market=condition_id))
279
+ return True
280
+ except Exception as e:
281
+ log.warning("cancel_market(%s) failed: %s", condition_id, e)
282
+ return False
283
+
284
+ def open_orders(self, condition_id=None):
285
+ try:
286
+ return self.client.get_open_orders(
287
+ self._t["OpenOrderParams"](market=condition_id)) or []
288
+ except Exception as e:
289
+ log.warning("get_open_orders failed: %s", e)
290
+ return None
291
+
292
+ def trades_totals(self, condition_id, token_id=None, side="BUY",
293
+ fee_rate=FEE_RATES["crypto"]):
294
+ """Exchange truth for one market: (shares, usd, fee_estimate) actually
295
+ traded on our account, or None if the API is unreachable. FAILED
296
+ trades are excluded; maker fills carry zero fee."""
297
+ try:
298
+ trades = self.client.get_trades(
299
+ self._t["TradeParams"](market=condition_id, asset_id=token_id))
300
+ except Exception as e:
301
+ log.warning("get_trades(%s) failed: %s", condition_id, e)
302
+ return None
303
+ sh = usd = fees = 0.0
304
+ for t in trades or []:
305
+ if not isinstance(t, dict) or t.get("side") != side:
306
+ continue
307
+ if t.get("status") == "FAILED":
308
+ continue
309
+ try:
310
+ s, p = float(t.get("size") or 0), float(t.get("price") or 0)
311
+ except (TypeError, ValueError):
312
+ continue
313
+ sh += s
314
+ usd += p * s
315
+ if t.get("trader_side") != "MAKER":
316
+ fees += fee(p, s, fee_rate)
317
+ return sh, usd, fees
318
+
319
+ def reconcile(self, condition_id, token_id=None):
320
+ """After :class:`OrderUncertain`: cancel anything possibly resting,
321
+ verify nothing stayed open, then return exchange-truth totals. Call
322
+ this BEFORE placing any new order on that market."""
323
+ self.cancel_market(condition_id)
324
+ still = self.open_orders(condition_id)
325
+ if still:
326
+ log.warning("reconcile(%s): %d orders still open after cancel, retrying",
327
+ condition_id, len(still))
328
+ self.cancel_market(condition_id)
329
+ return self.trades_totals(condition_id, token_id)
pmq/mcp.py ADDED
@@ -0,0 +1,188 @@
1
+ """pmq-mcp: MCP server exposing pmq to LLM agents, safety rails included.
2
+
3
+ Run with ``pmq-mcp`` (stdio transport). Design rules:
4
+
5
+ * Read tools work with no credentials and never touch keys.
6
+ * Trading tools exist ONLY when the operator sets ``PMQ_MCP_LIVE=1`` in the
7
+ server's environment. An agent cannot talk its way past a tool that was
8
+ never registered.
9
+ * Every order is capped by ``PMQ_MCP_MAX_USD`` (default 10) per call.
10
+ * On an unknown outcome (timeout, 5xx) the server reconciles automatically
11
+ and reports exchange truth instead of guessing.
12
+ * POLY_PRIVATE_KEY is read by the executor, used to sign, never returned.
13
+ """
14
+ import os
15
+ import urllib.parse
16
+
17
+ from . import data
18
+ from .exceptions import OrderUncertain
19
+
20
+ try:
21
+ from mcp.server.fastmcp import FastMCP
22
+ except ImportError as e: # pragma: no cover
23
+ raise SystemExit("pmq-mcp needs the optional dependency: pip install 'pmq[mcp]'") from e
24
+
25
+ LIVE_ENABLED = os.environ.get("PMQ_MCP_LIVE") == "1"
26
+ MAX_USD = float(os.environ.get("PMQ_MCP_MAX_USD", "10"))
27
+
28
+ mcp = FastMCP(
29
+ "pmq",
30
+ instructions=(
31
+ "Polymarket CLOB V2 data and fail-closed execution. Books are real "
32
+ "time; the trade tape lags 1 to 3 minutes, never trade off it. "
33
+ + ("Trading tools are ENABLED; every order is capped at "
34
+ f"{MAX_USD:.2f} USD per call." if LIVE_ENABLED else
35
+ "Trading tools are DISABLED (operator did not set PMQ_MCP_LIVE=1); "
36
+ "this server is read-only.")),
37
+ )
38
+
39
+ _executor = None
40
+
41
+
42
+ def _ex():
43
+ global _executor
44
+ if _executor is None:
45
+ from .executor import PolymarketExecutor
46
+ _executor = PolymarketExecutor()
47
+ return _executor
48
+
49
+
50
+ def _fill_dict(fill):
51
+ return {"order_id": fill.order_id, "matched_shares": fill.matched_shares,
52
+ "matched_usd": fill.matched_usd, "price": fill.price,
53
+ "rejected": fill.rejected, "error": fill.error,
54
+ "booked": bool(fill)}
55
+
56
+
57
+ @mcp.tool()
58
+ def find_markets(query: str = "", limit: int = 10) -> list:
59
+ """Discover tradeable Polymarket markets of ANY kind (politics, sports,
60
+ crypto, culture). With a query, full-text search; without, the most
61
+ active events by 24h volume. Returns event title plus, per market, the
62
+ slug to pass to the `market` tool and the outcome names."""
63
+ if query:
64
+ res = data.http_get_json(
65
+ f"{data.GAMMA}/public-search?q={urllib.parse.quote(query)}"
66
+ f"&limit_per_type={min(limit, 20)}") or {}
67
+ events = res.get("events") or []
68
+ else:
69
+ events = data.http_get_json(
70
+ f"{data.GAMMA}/events?closed=false&order=volume24hr&ascending=false"
71
+ f"&limit={min(limit, 20)}") or []
72
+ out = []
73
+ for ev in events[:limit]:
74
+ for m in (ev.get("markets") or [])[:4]:
75
+ pm = data.parse_market(m)
76
+ if pm:
77
+ out.append({"event": ev.get("title"), "market_slug": pm["slug"],
78
+ "outcomes": [pm["outcome_a"], pm["outcome_b"]],
79
+ "volume24hr": ev.get("volume24hr")})
80
+ return out or [{"error": "nothing found"}]
81
+
82
+
83
+ @mcp.tool()
84
+ def market(slug: str) -> dict:
85
+ """Resolve one Polymarket market by its gamma slug (any category, works
86
+ for expired short-lived markets too). Returns condition_id, the outcome
87
+ names mapped to their token ids (use those token ids with `book` and the
88
+ trading tools), the close time and the settled winner if resolution
89
+ already happened."""
90
+ pm = data.parse_market(data.get_market(slug))
91
+ if not pm:
92
+ return {"error": f"no market found for slug {slug!r}"}
93
+ return {"condition_id": pm["condition_id"],
94
+ "outcomes": {pm["outcome_a"]: pm["token_a"], pm["outcome_b"]: pm["token_b"]},
95
+ "end_ts": pm["end_ts"],
96
+ "settled_winner": data.resolved_winner(pm)}
97
+
98
+
99
+ @mcp.tool()
100
+ def book(token_id: str, depth_lo: float = 0.0, depth_hi: float = 1.0) -> dict:
101
+ """REAL-TIME order book summary for one outcome token: best bid/ask with
102
+ sizes, plus the $ notional of asks resting inside [depth_lo, depth_hi].
103
+ This endpoint is served by the matching engine; trust it over the trade
104
+ tape for any live decision."""
105
+ b = data.get_book(token_id)
106
+ if not b:
107
+ return {"error": "book unavailable"}
108
+ bid, bid_sz, ask, ask_sz = data.best_bid_ask(b)
109
+ return {"bid": bid, "bid_size": bid_sz, "ask": ask, "ask_size": ask_sz,
110
+ "ask_depth_usd_in_range": data.band_ask_depth_usd(b, depth_lo, depth_hi),
111
+ **data.book_meta(b)}
112
+
113
+
114
+ @mcp.tool()
115
+ def taker_fee(price: float, shares: float, category: str = "crypto") -> dict:
116
+ """Official Polymarket taker fee in $ (fee = rate * p * (1-p) * shares).
117
+ Categories and rates: crypto 0.07, sports 0.03, finance/politics/mentions/
118
+ tech 0.04, economics/culture/weather 0.05, geopolitics 0. Makers pay 0."""
119
+ rate = data.FEE_RATES.get(category)
120
+ if rate is None:
121
+ return {"error": f"unknown category, pick one of {sorted(data.FEE_RATES)}"}
122
+ return {"fee_usd": data.fee(price, shares, rate), "rate": rate,
123
+ "cost_per_share_incl_fee": price + data.fee(price, 1.0, rate)}
124
+
125
+
126
+ @mcp.tool()
127
+ def account_collateral() -> dict:
128
+ """Collateral (pUSD, $) the CLOB sees for the configured account. If this
129
+ is 0 while funds are on-chain, the operator's POLY_SIG_TYPE is wrong
130
+ (the Polymarket app's deposit wallet needs 3)."""
131
+ return {"collateral_usd": _ex().collateral()}
132
+
133
+
134
+ @mcp.tool()
135
+ def account_trades(condition_id: str, token_id: str = "") -> dict:
136
+ """Exchange-truth totals of OUR account's BUY trades on one market:
137
+ (shares, usd, fee estimate). This is the reconciliation source, use it
138
+ after any uncertainty instead of trusting local bookkeeping."""
139
+ totals = _ex().trades_totals(condition_id, token_id or None)
140
+ if totals is None:
141
+ return {"error": "trades endpoint unreachable"}
142
+ sh, usd, fees = totals
143
+ return {"shares": sh, "usd": usd, "fee_estimate": fees}
144
+
145
+
146
+ if LIVE_ENABLED:
147
+ @mcp.tool()
148
+ def fak_buy(token_id: str, price_cap: float, usd: float) -> dict:
149
+ """Place a fill-and-kill market BUY: spend up to `usd` at prices no
150
+ worse than `price_cap`. Nothing rests on the book. Book ONLY what
151
+ `matched_shares`/`matched_usd` report; `booked: false` means nothing
152
+ happened. Hard-capped per call by the operator's PMQ_MCP_MAX_USD."""
153
+ if usd > MAX_USD:
154
+ return {"error": f"refused: {usd} exceeds the {MAX_USD} USD per-order cap"}
155
+ try:
156
+ return _fill_dict(_ex().buy_fak(token_id, price_cap, usd))
157
+ except OrderUncertain as e:
158
+ return {"error": f"outcome unknown ({e}); call cancel_and_reconcile "
159
+ "on this market before any new order"}
160
+
161
+ @mcp.tool()
162
+ def fak_sell(token_id: str, price_floor: float, shares: float) -> dict:
163
+ """Fill-and-kill market SELL of `shares` at prices no worse than
164
+ `price_floor`. Same confirmation contract as fak_buy."""
165
+ try:
166
+ return _fill_dict(_ex().sell_fak(token_id, price_floor, shares))
167
+ except OrderUncertain as e:
168
+ return {"error": f"outcome unknown ({e}); "
169
+ "call account_trades before any new order"}
170
+
171
+ @mcp.tool()
172
+ def cancel_and_reconcile(condition_id: str, token_id: str = "") -> dict:
173
+ """Cancel every resting order of ours on one market, verify none
174
+ stayed open, and return exchange-truth totals. Call this after any
175
+ 'outcome unknown' before placing new orders on that market."""
176
+ totals = _ex().reconcile(condition_id, token_id or None)
177
+ if totals is None:
178
+ return {"cancelled": True, "error": "trades endpoint unreachable"}
179
+ sh, usd, fees = totals
180
+ return {"cancelled": True, "shares": sh, "usd": usd, "fee_estimate": fees}
181
+
182
+
183
+ def main():
184
+ mcp.run()
185
+
186
+
187
+ if __name__ == "__main__":
188
+ main()
@@ -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
@@ -0,0 +1,10 @@
1
+ pmq/__init__.py,sha256=PMuCY0rHDqRcuSJBpKgYIMqIwVfRc7Nm0LPF1YV8ig0,1274
2
+ pmq/data.py,sha256=GTOFGI-kMkycvO1cbTHVMY6Xdr90An86NJ8sJmPxQAU,7951
3
+ pmq/exceptions.py,sha256=A5O45Fv0Sa9whxb352pUHCKwvTyVFAMHznAhzrvkPk0,806
4
+ pmq/executor.py,sha256=EA4RYbzG2cZE70-XgyNGCKByBtyIleZjGk_uBnRSWo0,15602
5
+ pmq/mcp.py,sha256=u-B6NskztWDEun7Qc93psTQgpPUjE4UTCGosVvx3M94,7902
6
+ pmquant-0.1.0.dist-info/METADATA,sha256=NG6OcE4joCCXXIJ-Pdw2cbihBl6A19j3etN4r5ez2aM,9278
7
+ pmquant-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
8
+ pmquant-0.1.0.dist-info/entry_points.txt,sha256=KA0pbQsoNqTk8q05jSsalaEoxfTe9T_EzClj0rksTco,41
9
+ pmquant-0.1.0.dist-info/licenses/LICENSE,sha256=ONNtjWb-5LmWWI44Qg3AXhr4iDwHLVWEa0wHWOUSK3c,1064
10
+ pmquant-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pmq-mcp = pmq.mcp:main
@@ -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.